Compare commits

..

5 Commits

17 changed files with 952 additions and 245 deletions

View File

@ -1,22 +0,0 @@
from commands.command import Command
from evennia import CmdSet
class CmdRemove(Command):
key = "remove"
help_category = "clothing"
def func(self):
clothing = self.caller.search(self.args, candidates=self.caller.contents)
if not clothing:
self.caller.msg("You don't have anything like that.")
return
if not clothing.db.worn:
self.caller.msg("You're not wearing that!")
return
if covered := clothing.db.covered_by:
self.caller.msg(f"You have to take off {covered} first.")
return
if not clothing.access(self.caller, 'remove'):
self.caller.msg(clothing.db.remove_err_msg or f"You are unable to remove that.")
return
clothing.remove(self.caller)

View File

@ -9,7 +9,6 @@ from evennia.commands.command import Command as BaseCommand
# from evennia import default_cmds # from evennia import default_cmds
class Command(BaseCommand): class Command(BaseCommand):
""" """
Base command (you may see this if a child command had no help text defined) Base command (you may see this if a child command had no help text defined)
@ -32,6 +31,36 @@ class Command(BaseCommand):
# #
pass pass
class CmdMeow(Command):
key = 'meow'
def func(self):
args = self.args.strip()
if not args:
self.caller.msg("Who or what would you like to meow at?")
return
target = self.caller.search(args)
if not target:
return
self.caller.msg(f"You meowed at {target.key}!")
target.msg(f"You got meowed at by {self.caller.key}!")
class CmdBark(Command):
key = 'bark'
def func(self):
args = self.args.strip()
if not args:
self.caller.msg("Who or what would you like to bark at?")
return
target = self.caller.search(args)
if not target:
return
self.caller.msg(f"You barked at {target.key}!")
target.msg(f"You got barked at by {self.caller.key}!")
# ------------------------------------------------------------- # -------------------------------------------------------------
# #

15
commands/custom_cmdset.py Normal file
View File

@ -0,0 +1,15 @@
from evennia import CmdSet
#from commands import encounter_cmdset as encounter
from commands import command as cmd
class SetSpeciesHuman(CmdSet):
def at_cmdset_creation(self):
self.add(cmd.CmdMeow())
class SetSpeciesCatkin(CmdSet):
def at_cmdset_creation(self):
self.add(cmd.CmdMeow())
class SetSpeciesDogkin(CmdSet):
def at_cmdset_creation(self):
self.add(cmd.CmdBark())

View File

@ -16,10 +16,11 @@ own cmdsets by inheriting from them or directly from `evennia.CmdSet`.
from evennia.contrib.game_systems.clothing import ClothedCharacterCmdSet from evennia.contrib.game_systems.clothing import ClothedCharacterCmdSet
from evennia.contrib.game_systems.containers import ContainerCmdSet from evennia.contrib.game_systems.containers import ContainerCmdSet
from evennia.contrib.rpg.character_creator.character_creator import ContribCmdCharCreate
from evennia import default_cmds from evennia import default_cmds
from .encounter.engage import CmdEngage from .encounter_cmdset import CmdEngage
from .clothing import CmdRemove
class CharacterCmdSet(default_cmds.CharacterCmdSet): class CharacterCmdSet(default_cmds.CharacterCmdSet):
""" """
@ -38,7 +39,6 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
self.add(CmdEngage) self.add(CmdEngage)
self.add(ClothedCharacterCmdSet) self.add(ClothedCharacterCmdSet)
self.add(ContainerCmdSet) self.add(ContainerCmdSet)
self.add(CmdRemove)
class AccountCmdSet(default_cmds.AccountCmdSet): class AccountCmdSet(default_cmds.AccountCmdSet):
@ -52,14 +52,10 @@ class AccountCmdSet(default_cmds.AccountCmdSet):
key = "DefaultAccount" key = "DefaultAccount"
def at_cmdset_creation(self): def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation() super().at_cmdset_creation()
#
# any commands you add below will overload the default ones.
#
# Char creation
self.add(ContribCmdCharCreate)
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet): class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
""" """
@ -99,3 +95,4 @@ class SessionCmdSet(default_cmds.SessionCmdSet):
# #
# any commands you add below will overload the default ones. # any commands you add below will overload the default ones.
# #

View File

@ -1,41 +0,0 @@
from commands.command import Command
class EncounterCommand(Command):
energy_cost = 1
allow_self_target = False
def handler(self):
return self.caller.ndb.encounter_handler
def can_perform(self):
# Returns whether the caller is having their turn and if they have enough energy
# to make this action
return self.handler().can_act(self)
def has_target(self):
# Targeting code:
# Looks for the suggested target
# If none is found but there is only one other character in the encounter,
# default to that other character
# Else ask player for clarification
if self.target:
self.target = self.caller.search(self.target)
if not self.target:
default_target = self.handler().has_default_target(self.caller)
if default_target:
self.caller.msg("Defaulting to other character in encounter")
self.target = default_target
else:
self.caller.msg(f"Who are you trying to {self.key}?")
self.target = None
else:
default_target = self.handler().has_default_target(self.caller)
if default_target:
self.target = default_target
else:
self.caller.msg(f"Who are you trying to {self.key}?")
self.target = None
if self.caller == self.target and not self.allow_self_target:
self.msg(f"You aren't supposed to {self.key} yourself!")
return self.target

View File

@ -1,36 +0,0 @@
from commands.encounter import EncounterCommand
# Bite (Regular)
class CmdBite(EncounterCommand):
"Bite down on your target with your teeth"
key = "bite"
help_category = "Violence"
def parse(self):
self.args = self.args.strip()
target, *adverb = self.args.split(":")
self.target = target.strip()
self.adverb = ' ' + adverb[0].strip() if adverb else ''
def func(self):
if super().has_target() and super().can_perform():
self.caller.msg(f"You bite {self.target.key}{self.adverb}")
self.target.msg(f"{self.caller.key} bites you{self.adverb}")
# damage calculations, chance to miss, etc
# Kiss (Regular)
class CmdKiss(EncounterCommand):
"Kiss the target with your lips"
key = "kiss"
help_category = "Sex"
def parse(self):
self.args = self.args.strip()
target, *adverb = self.args.split(':')
self.target = target.strip()
self.adverb = ' ' + adverb[0].strip() if adverb else ''
def func(self):
if super().has_target() and super().can_perform():
self.caller.msg(f"You kiss {self.target}{self.adverb}")
self.target.msg(f"{self.caller} kisses you{self.adverb}")

View File

@ -1,36 +0,0 @@
from commands.command import Command
from evennia.utils import inherits_from
from typeclasses.characters import Character
from evennia.utils.create import create_script
class CmdEngage(Command):
"""
Initiates an encounter with the selected target
"""
key = "engage"
aliases = ["encounter", "fight"]
def func(self):
if not self.args:
self.caller.msg("Usage: engage <target>")
return
target = self.caller.search(self.args)
if not target:
return
if not inherits_from(target, Character) or not target.access(self.caller, 'engage'):
self.caller.msg(target.db.engage_err_msg or f"You can't initiate an encounter with {target.name}.")
return
if target == self.caller:
self.caller.msg("You can't initiate an encounter with yourself!")
return
if target.ndb.encounter_handler:
target.ndb.encounter_handler.add_character(self.caller)
target.ndb.encounter_handler.msg_all(f"{self.caller} joins the encounter")
else:
handler = create_script("encounter_handler.EncounterHandler")
handler.add_character(self.caller)
handler.add_character(target)
target.msg(f"{self.caller} has engaged you in an encounter!")
target.msg("You may leave using the flee command (or aliases)")
self.caller.msg(f"You have entered an encounter with {target}. Be polite!")
self.caller.msg("It is now your turn:")

View File

@ -1,7 +0,0 @@
from evennia import CmdSet
from .actions import *
class SetSpeciesHuman(CmdSet):
def at_cmdset_creation(self):
self.add(CmdBite)
self.add(CmdKiss)

View File

@ -1,88 +0,0 @@
from evennia import CmdSet
from commands.encounter import EncounterCommand
class CmdPass(EncounterCommand):
"Pass the turn over to the next character"
energy_cost = 0
key = "pass"
aliases = ["next", "finish", "done"]
help_category = "Encounter"
def func(self):
if super().can_perform():
self.caller.msg("You end your turn")
super().handler().next_turn()
class CmdRP(EncounterCommand):
"Free-form text input (for describing your actions in RP)"
energy_cost = 0
key = "rp"
arg_regex = None
aliases = [">"]
help_category = "Encounter"
def func(self):
if super().can_perform():
super().handler().msg_all(self.args.strip())
class CmdFlee(EncounterCommand):
"Exits the current encounter"
energy_cost = 0
key = "flee"
aliases = ["escape", "run", "run away", "leave"]
help_category = "Encounter"
def func(self):
super().handler().msg_all(f"{self.caller.key} left the encounter")
super().handler().remove_character(self.caller)
class CmdOOC(EncounterCommand):
"Say something out-of-character"
energy_cost = 0
key = "("
arg_regex = None
def func(self):
super().handler().msg_all(f"({self.caller}: {self.args.strip()})")
class CmdPose(EncounterCommand):
"""
strike a pose
Usage:
pose <pose text>
pose's <pose text>
Example:
pose is standing by the wall, smiling.
-> others will see:
Tom is standing by the wall, smiling.
Describe an action being taken. The pose text will
automatically begin with your name.
"""
key = "pose"
aliases = [":", "emote"]
locks = "cmd:all()"
arg_regex = None
energy_cost = 0
def parse(self):
args = self.args
if args and not args[0] in ["'", ",", ":"]:
args = " %s" % args.strip()
self.args = args
def func(self):
if super().can_perform():
if not self.args:
self.msg("What do you want to do?")
else:
msg = f"{self.caller.name}{self.args}"
super().handler().msg_all_rich((msg, {'type':'pose'}), self.caller)
class SetEncounterSpecial(CmdSet):
def at_cmdset_creation(self):
self.add(CmdPass)
self.add(CmdRP)
self.add(CmdFlee)
self.add(CmdOOC)
self.add(CmdPose)

View File

@ -0,0 +1,218 @@
from commands.command import Command
from evennia import create_script
from evennia import CmdSet
class EncounterCommand(Command):
energy_cost = 1
allow_self_target = False
def handler(self):
return self.caller.ndb.encounter_handler
def can_perform(self):
# Returns whether the caller is having their turn and if they have enough energy
# to make this action
return self.handler().can_act(self)
def has_target(self):
# Targeting code:
# Looks for the suggested target
# If none is found but there is only one other character in the encounter,
# default to that other character
# Else ask player for clarification
if self.target:
self.target = self.caller.search(self.target)
if not self.target:
default_target = self.handler().has_default_target(self.caller)
if default_target:
self.caller.msg("Defaulting to other character in encounter")
self.target = default_target
else:
self.caller.msg(f"Who are you trying to {self.key}?")
self.target = None
else:
default_target = self.handler().has_default_target(self.caller)
if default_target:
self.target = default_target
else:
self.caller.msg(f"Who are you trying to {self.key}?")
self.target = None
if self.caller == self.target and not self.allow_self_target:
self.msg(f"You aren't supposed to {self.key} yourself!")
return self.target
# Bite (Regular)
class CmdBite(EncounterCommand):
"Bite down on your target with your teeth"
key = "bite"
help_category = "Violence"
def parse(self):
self.args = self.args.strip()
target, *adverb = self.args.split(":")
self.target = target.strip()
self.adverb = ' ' + adverb[0].strip() if adverb else ''
def func(self):
if super().has_target() and super().can_perform():
self.caller.msg(f"You bite {self.target.key}{self.adverb}")
self.target.msg(f"{self.caller.key} bites you{self.adverb}")
# damage calculations, chance to miss, etc
# Kiss (Regular)
class CmdKiss(EncounterCommand):
"Kiss the target with your lips"
key = "kiss"
help_category = "Sex"
def parse(self):
self.args = self.args.strip()
target, *adverb = self.args.split(':')
self.target = target.strip()
self.adverb = ' ' + adverb[0].strip() if adverb else ''
def func(self):
if super().has_target() and super().can_perform():
self.caller.msg(f"You kiss {self.target}{self.adverb}")
self.target.msg(f"{self.caller} kisses you{self.adverb}")
class SetSpeciesHuman(CmdSet):
def at_cmdset_creation(self):
self.add(CmdBite)
self.add(CmdKiss)
class SetSpeciesCatkin(CmdSet):
def at_cmdset_creation(self):
self.add(CmdBite)
self.add(CmdKiss)
class SetSpeciesDogkin(CmdSet):
def at_cmdset_creation(self):
self.add(CmdBite)
self.add(CmdKiss)
# Special encounter commands
class CmdPass(EncounterCommand):
"Pass the turn over to the next character"
energy_cost = 0
key = "pass"
aliases = ["next", "finish", "done"]
help_category = "Encounter"
def func(self):
if super().can_perform():
self.caller.msg("You end your turn")
super().handler().next_turn()
class CmdRP(EncounterCommand):
"Free-form text input (for describing your actions in RP)"
energy_cost = 0
key = "rp"
arg_regex = None
aliases = [">"]
help_category = "Encounter"
def func(self):
if super().can_perform():
super().handler().msg_all(self.args.strip())
class CmdFlee(EncounterCommand):
"Exits the current encounter"
energy_cost = 0
key = "flee"
aliases = ["escape", "run", "run away", "leave"]
help_category = "Encounter"
def func(self):
super().handler().msg_all(f"{self.caller.key} left the encounter")
super().handler().remove_character(self.caller)
class CmdOOC(EncounterCommand):
"Say something out-of-character"
energy_cost = 0
key = "("
arg_regex = None
def func(self):
super().handler().msg_all(f"({self.caller}: {self.args.strip()})")
class CmdPose(EncounterCommand):
"""
strike a pose
Usage:
pose <pose text>
pose's <pose text>
Example:
pose is standing by the wall, smiling.
-> others will see:
Tom is standing by the wall, smiling.
Describe an action being taken. The pose text will
automatically begin with your name.
"""
key = "pose"
aliases = [":", "emote"]
locks = "cmd:all()"
arg_regex = None
energy_cost = 0
def parse(self):
args = self.args
if args and not args[0] in ["'", ",", ":"]:
args = " %s" % args.strip()
self.args = args
def func(self):
if super().can_perform():
if not self.args:
self.msg("What do you want to do?")
else:
msg = f"{self.caller.name}{self.args}"
super().handler().msg_all_rich((msg, {'type':'pose'}), self.caller)
class SetEncounterSpecial(CmdSet):
def at_cmdset_creation(self):
self.add(CmdPass)
self.add(CmdRP)
self.add(CmdFlee)
self.add(CmdOOC)
self.add(CmdPose)
self.add(CmdKiss)
self.add(CmdBite)
# Encounter-related character commands
class CmdEngage(Command):
"""
Initiates an encounter with the selected target
"""
key = "engage"
aliases = ["encounter", "fight"]
def func(self):
if not self.args:
self.caller.msg("Usage: engage <target>")
return
target = self.caller.search(self.args)
if not target:
return
if target == self.caller:
self.caller.msg("You can't initiate an encounter with yourself!")
return
if target.ndb.encounter_handler:
target.ndb.encounter_handler.add_character(self.caller)
target.ndb.encounter_handler.msg_all(f"{self.caller} joins the encounter")
else:
handler = create_script("encounter_handler.EncounterHandler")
handler.add_character(self.caller)
handler.add_character(target)
target.msg(f"{self.caller} has engaged you in an encounter!")
target.msg("You may leave using the flee command (or aliases)")
self.caller.msg(f"You have entered an encounter with {target}. Be polite!")
self.caller.msg("It is now your turn:")

View File

@ -33,9 +33,15 @@ from evennia.settings_default import *
# This is the name of your game. Make it catchy! # This is the name of your game. Make it catchy!
SERVERNAME = "Multi-User Depravity" SERVERNAME = "Multi-User Depravity"
IDLE_TIMEOUT = -1 IDLE_TIMEOUT = -1
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
AUTO_PUPPET_ON_LOGIN = False
CHARGEN_MENU = "world.example_menu"
# Custom Traits
#TRAIT_CLASS_PATHS = ["world.traits.EarTrait"]
###################################################################### ######################################################################
# Settings given in secret_settings.py override those in this file. # Settings given in secret_settings.py override those in this file.
###################################################################### ######################################################################

View File

@ -23,9 +23,10 @@ several more options for customizing the Guest account system.
""" """
from evennia.accounts.accounts import DefaultAccount, DefaultGuest from evennia.accounts.accounts import DefaultAccount, DefaultGuest
from evennia.contrib.rpg.character_creator.character_creator import ContribChargenAccount
class Account(DefaultAccount): class Account(ContribChargenAccount):
""" """
This class describes the actual OOC account (i.e. the user connecting This class describes the actual OOC account (i.e. the user connecting
to the MUD). It does NOT have visual appearance in the game world (that to the MUD). It does NOT have visual appearance in the game world (that
@ -102,3 +103,4 @@ class Guest(DefaultGuest):
""" """
pass pass

View File

@ -1,8 +1,10 @@
from evennia.contrib.game_systems.clothing import ClothedCharacter from evennia.contrib.game_systems.clothing import ClothedCharacter
from evennia.contrib.rpg.traits import TraitHandler from evennia.contrib.rpg.traits import TraitHandler
#from evennia.contrib.rpg.traits import TraitProperty
#from evennia.utils import lazy_property
from commands.encounter.special import SetEncounterSpecial from commands.encounter_cmdset import SetEncounterSpecial
from world.species import SPECIES_CMDSET from world.species import SPECIES_CMDSET
CURRENT_VERSION = 1 CURRENT_VERSION = 1
@ -11,16 +13,25 @@ class Character(ClothedCharacter):
def at_object_creation(self): def at_object_creation(self):
self.db.version = 1 self.db.version = 1
self.db.species = 0 self.db.species = 0
# ears = TraitProperty("Ears", trait_type="counter",
# base=0, mod=0, min=0, max=2,
# descs = {0:"human ears", 1:"cat ears", 2:"dog ears"})
# tail = TraitProperty("Tail", trait_type="counter",
# base=0, mod=0, min=0, max=2,
# descs = {0:"no tail", 1:"cat tail", 2:"dog tail"})
def apply_encounter_cmdset(self): def apply_encounter_cmdset(self):
self.cmdset.add(SetEncounterSpecial) self.cmdset.add(SetEncounterSpecial)
self.cmdset.add(SPECIES_CMDSET[self.db.species]) self.cmdset.add(SPECIES_CMDSET[self.db.species])
def revoke_encounter_cmdset(self): def revoke_encounter_cmdset(self):
self.cmdset.remove(SPECIES_CMDSET[self.db.species]) # self.cmdset.remove(SPECIES_CMDSET[self.db.species])
self.cmdset.remove(SetEncounterSpecial) self.cmdset.remove(SetEncounterSpecial)
pass pass
def apply_basic_cmdset(self):
self.cmdset.add(SPECIES_CMDSET[self.db.species])
def at_pre_move(self, destination, **kwargs): def at_pre_move(self, destination, **kwargs):
if self.ndb.encounter_handler: if self.ndb.encounter_handler:
self.msg("You would need to leave the encounter first") self.msg("You would need to leave the encounter first")

View File

@ -11,9 +11,34 @@ inheritance.
""" """
from evennia.objects.objects import DefaultObject from evennia.objects.objects import DefaultObject
from evennia.utils import lazy_property
from evennia.contrib.rpg.traits import TraitProperty
from evennia.contrib.rpg.traits import TraitHandler
class ObjectParent: class ObjectParent:
pass pass
class Object(ObjectParent, DefaultObject): class Object(ObjectParent, DefaultObject):
pass pass
#class ObjectPlayerDefault(DefaultObject):
#class Object(ObjectParent, DefaultObject):
#
## @lazy_property
## def traits(self):
## return TraitHandler(self)
##
## @lazy_property
## def looks(self):
## return TraitHandler(self, db_attribute_key="looks")
#
# def at_object_creation(self):
# self.looks.add("ears", "Ears", trait_type="counter",
# base=0, mod=0, min=0, max=2,
# descs = {0:"human ears", 1:"cat ears", 2:"dog ears"})
# self.looks.add("tail", "Tail", trait_type="counter",
# base=0, mod=0, min=0, max=2,
# descs = {0:"no tail", 1:"cat tail", 2:"dog tail"})
#
# def return_looks(self):
# pass

9
world/bodyparts.py Normal file
View File

@ -0,0 +1,9 @@
from commands.encounter_cmdset import SetSpeciesHuman
SPECIES_LIST = [
"Human",
]
SPECIES_CMDSET = [
SetSpeciesHuman,
]

615
world/example_menu.py Normal file
View File

@ -0,0 +1,615 @@
"""
An example EvMenu for the character creator contrib.
This menu is not intended to be a full character creator, but to demonstrate
several different useful node types for your own creator. Any of the different
techniques demonstrated here can be combined into a single decision point.
## Informational Pages
A set of nodes that let you page through information on different choices.
The example shows how to have a single informational page for each option, but
you can expand that into sub-categories by setting the values to dictionaries
instead of simple strings and referencing the "Option Categories" nodes.
## Option Categories
A pair of nodes which let you divide your options into separate categories.
The base node has a list of categories as the options, which is passed to the
child node. That child node then presents the options for that category and
allows the player to choose one.
## Multiple Choice
Allows players to select and deselect options from the list in order to choose
more than one. The example has a requirement of choosing exactly 3 options,
but you can change it to a maximum or minimum number of required options -
or remove the requirement check entirely.
## Simple List Options
If you just want a straightforward list of options, without any of the back-and-forth
navigation or modifying of option text, evennia has an easy to use decorator
available: `@list_node`
For an example of how to use it, check out the documentation for evennia.utils.evmenu
- there's lots of other useful EvMenu tools too!
## Starting Objects
Allows players to choose from a selection of starting objects.
When creating starting objects e.g. gear, it's best to actually create them
at the end, so you don't have to patch checks in for orphaned objects or
infinite-object player exploits.
## Choosing a Name
The contrib character create command assumes the player will choose their name
during character creation. This section validates name choices before confirming
them.
## The End
It might not seem like an important part, since the players don't make a decision
here, but the final node of character creation is where you finalize all of
the decisions players made earlier. Initializing skills, creating starting gear,
and other one-time method calls and set-up should be put here.
"""
import inflect
from typeclasses.characters import Character
from evennia.contrib.rpg.traits import TraitProperty
from evennia.prototypes.spawner import spawn
from evennia.utils import dedent
from evennia.utils.evtable import EvTable
_INFLECT = inflect.engine()
#########################################################
# Welcome Page
#########################################################
def menunode_welcome(caller):
"""Starting page."""
text = dedent(
"""\
|wWelcome to Character Creation!|n
This is the starting node for all brand new characters. It's a good place to
remind players that they can exit the character creator and resume later,
especially if you're going to have a really long chargen process.
A brief overview of the game could be a good idea here, too, or a link to your
game wiki if you have one.
"""
)
help = "You can explain the commands for exiting and resuming more specifically here."
options = {"desc": "Let's begin!", "goto": "menunode_info_base"}
return (text, help), options
#########################################################
# Informational Pages
#########################################################
# Storing your information in a dictionary like this makes the menu nodes much cleaner,
# as well as making info easier to update. You can even import it from a different module,
# e.g. wherever you have the classes actually defined, so later updates only happen in one place.
_SPECIES_INFO_DICT = {
"Human": dedent(
"""\
The most common species in the adventurers guild, at least for new recruits.
Humans are well-rounded and very adaptable.
While they may not have claws or fangs or any fancy adaptations,
they don't have any drawbacks or glaring weaknesses either.
"""
),
"Catkin": dedent(
"""\
Relatively common, compared to more obscure Monsterkin types.
A variant of human descended from feline heritage,
Catkin keep some traits of their ancestors, including ears, tails, and claws.
"""
),
"Dogkin": dedent(
"""\
Relatively common, compared to more obscure Monsterkin types.
A variant of human descended from canine heritage,
Dogkin keep some traits of their ancestors, including ears, tails, and fangs.
"""
)
# Descriptions and species still very much WIP
}
def menunode_info_base(caller):
"""Base node for the informational choices."""
# this is a base node for a decision, so we want to save the character's progress here
caller.new_char.db.chargen_step = "menunode_info_base"
text = dedent(
"""\
|wInformational Pages|n
Sometimes you'll want to let players read more about options before choosing
one of them. This is especially useful for big choices like race or class.
"""
)
help = "A link to your wiki for more information on classes could be useful here."
options = []
# Build your options from your info dict so you don't need to update this to add new options
for species in _SPECIES_INFO_DICT.keys():
options.append(
{
"desc": f"Learn about the |c{species}|n species",
"goto": ("menunode_info_species", {"selected_species": species}),
}
)
return (text, help), options
# putting your kwarg in the menu declaration helps keep track of what variables the node needs
def menunode_info_species(caller, raw_string, selected_species=None, **kwargs):
"""Informational overview of a particular species"""
# sometimes weird bugs happen - it's best to check for them rather than let the game break
if not selected_species:
# reset back to the previous step
caller.new_char.db.chargen_step = "menunode_welcome"
# print error to player and quit the menu
return "Something went wrong. Please try again."
# Since you have all the info in a nice dict, you can just grab it to display here
text = _SPECIES_INFO_DICT[selected_species]
help = "If you want option-specific help, you can define it in your info dict and reference it."
options = []
# set an option for players to choose this class
options.append(
{
"desc": f"Become {_INFLECT.an(selected_species)}",
"goto": (_set_species, {"selected_species": selected_species}),
}
)
# once again build your options from the same info dict
for species in _SPECIES_INFO_DICT.keys():
# make sure you don't print the currently displayed page as an option
if species != selected_species:
options.append(
{
"desc": f"Learn about the |c{species}|n species",
"goto": ("menunode_info_species", {"selected_species": species}),
}
)
return (text, help), options
def _set_species(caller, raw_string, selected_species=None, **kwargs):
# a species should always be selected here
if not selected_species:
# go back to the base node for this decision
return "menunode_info_base"
char = caller.new_char
# any special code for setting this option would go here!
# but we'll just set an attribute
char.db.species = list(_SPECIES_INFO_DICT.keys()).index(selected_species)
# move on to the next step!
return "menunode_multi_choice"
#########################################################
# Option Categories
#########################################################
# for these subcategory options, we make a dict of categories and option lists
_APPEARANCE_DICT = {
# the key is your category; the value is a list of options, in the order you want them to appear
"body type": [
"skeletal",
"skinny",
"slender",
"slim",
"athletic",
"muscular",
"broad",
"round",
"curvy",
"stout",
"chubby",
],
"height": ["diminutive", "short", "average", "tall", "towering"],
}
def menunode_categories(caller, **kwargs):
"""Base node for categorized options."""
# this is a new decision step, so save your resume point here
caller.new_char.db.chargen_step = "menunode_categories"
text = dedent(
"""\
|wOption Categories|n
Some character attributes are part of the same mechanic or decision,
but need to be divided up into sub-categories. Character appearance
details are an example of where this can be useful.
"""
)
help = "Some helpful extra information on what's affected by these choices works well here."
options = []
# just like for informational categories, build the options off of a dictionary to make it
# easier to manage
for category in _APPEARANCE_DICT.keys():
options.append(
{
"desc": f"Choose your |c{category}|n",
"goto": ("menunode_category_options", {"category": category}),
}
)
# since this node goes in and out of sub-nodes, you need an option to proceed to the next step
options.append(
{
"key": ("(Next)", "next", "n"),
"desc": "Continue to the next step.",
"goto": "menunode_multi_choice",
}
)
# once past the first decision, it's also a good idea to include a "back to previous step"
# option
options.append(
{
"key": ("(Back)", "back", "b"),
"desc": "Go back to the previous step",
"goto": "menunode_info_base",
}
)
return (text, help), options
def menunode_category_options(caller, raw_string, category=None, **kwargs):
"""Choosing an option within the categories."""
if not category:
# this shouldn't have happened, so quit and retry
return "Something went wrong. Please try again."
# for mechanics-related choices, you can combine this with the
# informational options approach to give specific info
text = f"Choose your {category}:"
help = f"This will define your {category}."
options = []
# build the list of options from the right category of your dictionary
for option in _APPEARANCE_DICT[category]:
options.append(
{"desc": option, "goto": (_set_category_opt, {"category": category, "value": option})}
)
# always include a "back" option in case they aren't ready to pick yet
options.append(
{
"key": ("(Back)", "back", "b"),
"desc": f"Don't change {category}",
"goto": "menunode_categories",
}
)
return (text, help), options
def _set_category_opt(caller, raw_string, category, value, **kwargs):
"""Set the option for a category"""
# this is where you would put any more complex code involved in setting the option,
# but we're just doing simple attributes
caller.new_char.attributes.add(category, value)
# go back to the base node for the categories choice to pick another
return "menunode_categories"
#########################################################
# Multiple Choice
#########################################################
# it's not as important to make this a separate list, but like all the others,
# it's easier to read and to update if you do!
_SKILL_OPTIONS = [
"alchemy",
"archery",
"blacksmithing",
"brawling",
"dancing",
"fencing",
"pottery",
"tailoring",
"weaving",
]
def menunode_multi_choice(caller, raw_string, **kwargs):
"""A multiple-choice menu node."""
char = caller.new_char
# another decision, so save the resume point
char.db.chargen_step = "menunode_multi_choice"
# in order to support picking up from where we left off, get the options from the character
# if they weren't passed in
# this is again just a simple attribute, but you could retrieve this list however
selected = kwargs.get("selected") or char.attributes.get("skill_list", [])
text = dedent(
"""\
|wMultiple Choice|n
Sometimes you want players to be able to pick more than one option -
for example, starting skills.
You can easily define it as a minimum, maximum, or exact number of
selected options.
"""
)
help = (
"This is a good place to specify how many choices are allowed or required. This example"
" requires exactly 3."
)
options = []
for option in _SKILL_OPTIONS:
# check if the option has been selected
if option in selected:
# if it's been selected, we want to highlight that
opt_desc = f"|y{option} (selected)|n"
else:
opt_desc = option
options.append(
{"desc": opt_desc, "goto": (_set_multichoice, {"selected": selected, "option": option})}
)
# only display the Next option if the requirements are met!
# for this example, you need exactly 3 choices, but you can use an inequality
# for "no more than X", or "at least X"
if len(selected) == 3:
options.append(
{
"key": ("(Next)", "next", "n"),
"desc": "Continue to the next step",
"goto": "menunode_choose_objects",
}
)
options.append(
{
"key": ("(Back)", "back", "b"),
"desc": "Go back to the previous step",
"goto": "menunode_categories",
}
)
return (text, help), options
def _set_multichoice(caller, raw_string, selected=[], **kwargs):
"""saves the current choices to the character"""
# get the option being chosen
if option := kwargs.get("option"):
# if the option is already in the list, then we want to remove it
if option in selected:
selected.remove(option)
# otherwise, we're adding it
else:
selected.append(option)
# now that the options are updated, save it to the character
# this is just setting an attribute but it could be anything
caller.new_char.attributes.add("skill_list", selected)
# pass the list back so we don't need to retrieve it again
return ("menunode_multi_choice", {"selected": selected})
#########################################################
# Starting Objects
#########################################################
# for a real game, you would most likely want to build this list from
# your existing game prototypes module(s), but for a demo this'll do!
_EXAMPLE_PROTOTYPES = [
# starter sword prototype
{
"key": "basic sword",
"desc": "This sword will do fine for stabbing things.",
"tags": [("sword", "weapon")],
},
# starter staff prototype
{
"key": "basic staff",
"desc": "You could hit things with it, or maybe use it as a spell focus.",
"tags": [("staff", "weapon"), ("staff", "focus")],
},
]
# this method will be run to create the starting objects
def create_objects(character):
"""do the actual object spawning"""
# since our example chargen saves the starting prototype to an attribute, we retrieve that here
proto = dict(character.db.starter_weapon)
# set the location to our character, so they actually have it
proto["location"] = character
# create the object
spawn(proto)
def menunode_choose_objects(caller, raw_string, **kwargs):
"""Selecting objects to start with"""
char = caller.new_char
# another decision, so save the resume point
char.db.chargen_step = "menunode_choose_objects"
text = dedent(
"""\
|wStarting Objects|n
Whether it's a cosmetic outfit, a starting weapon, or a professional
tool kit, you probably want to let your players have a choice in
what objects they start out with.
"""
)
help = (
"An overview of what the choice affects - whether it's purely aesthetic or mechanical, and"
" whether you can change it later - are good here."
)
options = []
for proto in _EXAMPLE_PROTOTYPES:
# use the key as the option description, but pass the whole prototype
options.append(
{
"desc": f"Choose {_INFLECT.an(proto['key'])}",
"goto": (_set_object_choice, {"proto": proto}),
}
)
options.append(
{
"key": ("(Back)", "back", "b"),
"desc": "Go back to the previous step",
"goto": "menunode_multi_choice",
}
)
return (text, help), options
def _set_object_choice(caller, raw_string, proto, **kwargs):
"""Save the selected starting object(s)"""
# we DON'T want to actually create the object, yet! that way players can still go back and
# change their mind instead, we save what object was chosen - in this case, by saving the
# prototype dict to the character
caller.new_char.db.starter_weapon = proto
# continue to the next step
return "menunode_choose_name"
#########################################################
# Choosing a Name
#########################################################
def menunode_choose_name(caller, raw_string, **kwargs):
"""Name selection"""
char = caller.new_char
# another decision, so save the resume point
char.db.chargen_step = "menunode_choose_name"
# check if an error message was passed to the node. if so, you'll want to include it
# into your "name prompt" at the end of the node text.
if error := kwargs.get("error"):
prompt_text = f"{error}. Enter a different name."
else:
# there was no error, so just ask them to enter a name.
prompt_text = "Enter a name here to check if it's available."
# this will print every time the player is prompted to choose a name,
# including the prompt text defined above
text = dedent(
f"""\
|wChoosing a Name|n
Especially for roleplaying-centric games, being able to choose your
character's name after deciding everything else, instead of before,
is really useful.
{prompt_text}
"""
)
help = "You'll have a chance to change your mind before confirming, even if the name is free."
# since this is a free-text field, we just have the one
options = {"key": "_default", "goto": _check_charname}
return (text, help), options
def _check_charname(caller, raw_string, **kwargs):
"""Check and confirm name choice"""
# strip any extraneous whitespace from the raw text
# if you want to do any other validation on the name, e.g. no punctuation allowed, this
# is the place!
charname = raw_string.strip()
# aside from validation, the built-in normalization function from the caller's Account does
# some useful cleanup on the input, just in case they try something sneaky
charname = caller.account.normalize_username(charname)
# check to make sure that the name doesn't already exist
candidates = Character.objects.filter_family(db_key__iexact=charname)
if len(candidates):
# the name is already taken - report back with the error
return (
"menunode_choose_name",
{"error": f"|w{charname}|n is unavailable.\n\nEnter a different name."},
)
else:
# it's free! set the character's key to the name to reserve it
caller.new_char.key = charname
# continue on to the confirmation node
return "menunode_confirm_name"
def menunode_confirm_name(caller, raw_string, **kwargs):
"""Confirm the name choice"""
char = caller.new_char
# since we reserved the name by assigning it, you can reference the character key
# if you have any extra validation or normalization that changed the player's input
# this also serves to show the player exactly what name they'll get
text = f"|w{char.key}|n is available! Confirm?"
# let players change their mind and go back to the name choice, if they want
options = [
{"key": ("Yes", "y"), "goto": "menunode_end"},
{"key": ("No", "n"), "goto": "menunode_choose_name"},
]
return text, options
#########################################################
# The End
#########################################################
def menunode_end(caller, raw_string):
"""End-of-chargen cleanup."""
char = caller.new_char
# since everything is finished and confirmed, we actually create the starting objects now
create_objects(char)
# clear in-progress status
caller.new_char.attributes.remove("chargen_step")
text = dedent(
"""
Congratulations!
You have completed character creation. Enjoy the game!
"""
)
return text, None

View File

@ -1,9 +1,19 @@
from commands.encounter.sets import SetSpeciesHuman from commands.encounter_cmdset import SetSpeciesHuman
from commands.encounter_cmdset import SetSpeciesCatkin
from commands.encounter_cmdset import SetSpeciesDogkin
from commands.custom_cmdset import SetSpeciesHuman
from commands.custom_cmdset import SetSpeciesCatkin
from commands.custom_cmdset import SetSpeciesDogkin
SPECIES_LIST = [ SPECIES_LIST = [
"Human", "Human",
"Catkin",
"Dogkin"
] ]
SPECIES_CMDSET = [ SPECIES_CMDSET = [
SetSpeciesHuman, SetSpeciesHuman,
SetSpeciesCatkin,
SetSpeciesDogkin,
] ]