Character creation system

Also fixed many bugs and inconsistencies. Including:
  - Whisper works with RP system
  - Saying /to to non-player character just uses what they wrote
  - Minor yell bug
  - Actions on Things (like the puddle) work with RP system
This commit is contained in:
Howard Abrams 2025-05-01 10:06:53 -07:00
parent a9e0c24ad9
commit 060abf846c
15 changed files with 665 additions and 87 deletions

View file

@ -18,8 +18,9 @@ from evennia import default_cmds
from evennia.contrib.grid import extended_room
from evennia.contrib.game_systems.gendersub import SetGender
from evennia.contrib.rpg.rpsystem import RPSystemCmdSet
from evennia.contrib.rpg.character_creator.character_creator import ContribChargenCmdSet
from commands.sittables import CmdNoSitStand
from commands.everyone import CmdTake, CmdThink, CmdSay
from commands.everyone import CmdTake, CmdThink, CmdSay, CmdWhisper
from commands.wizards import CmdGM
class CharacterCmdSet(default_cmds.CharacterCmdSet):
@ -37,12 +38,13 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
"""
super().at_cmdset_creation()
self.add(CmdNoSitStand)
self.add(CmdTake())
self.add(CmdThink())
self.add(SetGender())
self.add(RPSystemCmdSet())
self.add(CmdTake)
self.add(CmdThink)
self.add(SetGender)
self.add(RPSystemCmdSet)
self.add(extended_room.ExtendedRoomCmdSet)
self.add(CmdSay())
self.add(CmdSay)
self.add(CmdWhisper)
self.add(CmdGM)
@ -64,6 +66,7 @@ class AccountCmdSet(default_cmds.AccountCmdSet):
#
# any commands you add below will overload the default ones.
#
self.add(ContribChargenCmdSet)
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):

View file

@ -9,15 +9,53 @@ from evennia.commands.default.muxcommand import MuxCommand
from evennia.contrib.rpg.rpsystem import send_emote
from evennia.utils import iter_to_str, logger
from utils.word_list import routput
from utils.word_list import routput, paragraph
class CmdWhisper(MuxCommand):
"""
Speak privately as your character to another
Usage:
whisper <character> = <message>
whisper <char1>, <char2> = <message>
Talk privately to one or more characters in your current location, without
others in the room being informed.
"""
key = "whisper"
priority = 0
locks = "cmd:all()"
rhs_split = ("=")
def func(self):
"""
Implements the new 'whisper' command.
"""
if not self.args:
self.caller.msg("What are you whispering?")
return
if not self.rhs:
self.caller.msg("Usage: whisper <character> = <message>")
return
targets = [self.caller.search(target) for target in split(r" *, *", self.lhs)]
full_speech = f"/Me whispers to you, \"{self.rhs}\""
send_emote(self.caller, targets, full_speech, msg_type="say", anonymous_add=None, quiet=True)
to_list = [target.get_display_name(self) for target in targets]
full_speech = f"You whisper to {iter_to_str(to_list, endsep='and')}, \"{self.rhs}\""
self.caller.msg(full_speech, from_obj=self.caller)
class CmdSay(MuxCommand):
"""Say something to the characters in the same area.
"""
Say something to the characters in the same area.
Usage:
say phrase
say/to char1 [char2 ...], phrase
say/to char1, [char2 ...] = phrase
say[/switches] phrase
Where switches can be any of the following:
@ -25,8 +63,11 @@ class CmdSay(MuxCommand):
- yell : To replace 'says' with 'yells'
- scream : To replace 'says' with 'screams'
- ask : To replace 'says' with 'asks'
- to : Takes one or more characters in the same area, and directs the statement to them. Note that others can still hear the statement (see the 'whisper' command).
- adverb : Any adverb-like word that ends in '-ly' is added to the say command, for instance:
- to : Directs phrase to one or more characters
in the same area. Note others can still hear
the statement (see the 'whisper' command).
- adverb : Any adverb-like word that ends in '-ly'
is added to the say command, for instance:
say/quietly Hi there.
@ -40,13 +81,21 @@ class CmdSay(MuxCommand):
priority = 0
locks = "cmd:all()"
rhs_split = ("=")
arg_regex = None
def func(self):
"""
Implements the new 'say' command with switches.
"""
def charname(name):
try:
results = self.caller.search(name, quiet=True)
return results.get_display_name(self)
except:
return name
def chars_for_self(chars):
return [self.caller.search(c).get_display_name(self) for c in split(r"[ ,]+", chars)]
return [charname(c) for c in split(r"[ ,]+", chars)]
def chars_for_others(chars):
return [c if c.startswith('/') else '/'+c for c in split(r"[ ,]+", chars)]
@ -56,12 +105,20 @@ class CmdSay(MuxCommand):
return (' to ' if verb == 'say' else ' ') + \
iter_to_str(char_lst, endsep='and')
logger.info(f"CmdSayIt: {self.cmdstring} lhs={self.lhs} switches={self.switches}")
# logger.info(f"CmdSayIt: {self.cmdstring} lhs={self.lhs} switches={self.switches}")
if not self.args:
self.caller.msg("Say what?")
return
if 'to' in self.switches and not self.rhs:
self.caller.msg(paragraph("""
When attempting to say something to one or more
characters, use the '=' character to identify what you
want to say.
"""))
return
if self.rhs:
speech = self.rhs
else:
@ -76,10 +133,10 @@ class CmdSay(MuxCommand):
if switch.endswith('ly'):
adverb = switch + ' '
if 'yell' in self.switches or self.cmdstring == 'yell' or speech.endswith('!'):
verb = 'yell'
elif 'scream' in self.switches or self.cmdstring == 'scream':
if 'scream' in self.switches or self.cmdstring == 'scream':
verb = 'scream'
elif 'yell' in self.switches or self.cmdstring == 'yell' or speech.endswith('!'):
verb = 'yell'
elif 'ask' in self.switches or self.cmdstring == 'ask' or speech.endswith('?'):
verb = 'ask'
else:
@ -96,7 +153,9 @@ class CmdSay(MuxCommand):
targets = [item for item in self.caller.location.contents if item != self.caller]
to_whom = chars_list(self.lhs, verb) if 'to' in self.switches else ''
full_speech = f"/Me {adverb}{verb}s{to_whom}, \"{speech}\""
send_emote(self.caller, targets, full_speech, msg_type="say", anonymous_add=None)
send_emote(self.caller, targets, full_speech, msg_type="say", anonymous_add=None, quiet=True)
class CmdThink(Command, NumberedTargetCommand):
@ -130,6 +189,7 @@ class CmdThink(Command, NumberedTargetCommand):
else:
msg = f"{self.caller.name} . o O ( {thought} )"
self.caller.db.thinking_count = (self.caller.db.thinking_count or 0) + 1
self.caller.location.msg_contents(msg)

View file

@ -53,6 +53,11 @@ FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = True
SEARCH_MULTIMATCH_REGEX = r"(?P<number>[0-9]+)-(?P<name>[^-]*)(?P<args>.*)"
SEARCH_MULTIMATCH_TEMPLATE = " {number}-{name}{aliases}{info}\n"
CHARGEN_MENU = "world.chargen_menu"
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
AUTO_PUPPET_ON_LOGIN = True
MAX_NR_CHARACTERS = 6
######################################################################
# 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.contrib.rpg.character_creator.character_creator import ContribChargenAccount
class Account(DefaultAccount):
class Account(ContribChargenAccount):
"""
An Account is the actual OOC player entity. It doesn't exist in the game,
but puppets characters.
@ -135,8 +136,16 @@ class Account(DefaultAccount):
- at_post_chnnel_msg(message, channel, senders=None, **kwargs)
"""
def at_post_login(self, session=None, **kwargs):
protocol_flags = self.attributes.get("_saved_protocol_flags", {})
if session and protocol_flags:
session.update_flags(**protocol_flags)
pass
try:
self.puppet_object(session, self.db._last_puppet)
except RuntimeError:
self.msg("Welcome, welcome. Let's begin by creating you a character.")
self.execute_cmd("charcreate")
class Guest(DefaultGuest):

View file

@ -12,6 +12,7 @@ from re import match
from evennia.contrib.game_systems.gendersub import GenderCharacter
from evennia.contrib.rpg.rpsystem import ContribRPCharacter
from evennia.contrib.rpg.rpsystem import send_emote
from evennia.prototypes.spawner import spawn
from evennia.utils import delay # , logger
@ -56,6 +57,8 @@ class Character(Object, GenderCharacter, ContribRPCharacter):
See mygame/typeclasses/objects.py for a list of
properties and methods available on all Object child classes like this.
"""
pose = True
def at_object_creation(self):
"called when a character is first created."
self.db.tutorstate = 0
@ -105,9 +108,6 @@ class Character(Object, GenderCharacter, ContribRPCharacter):
thing = victim.has(to_take)
if thing and thing.db.can_take:
self.msg(f"You take {thing.key} from {victim.key}.")
self.location.msg_contents(
f"{self.key} takes {thing.key} from {victim.key}!",
exclude=self)
thing.move_to(self, quiet=True, use_destination=True)
return
@ -182,6 +182,13 @@ class Character(Object, GenderCharacter, ContribRPCharacter):
# function call.
return super().at_look(target)
def announce_action(self, message):
"""
Replaces a location's 'msg_contents' with an emote.
"""
targets = [item for item in self.location.contents if item != self]
send_emote(self, targets, f"/Me {message}", msg_type="say", anonymous_add=None, quiet=True)
def spell_sequence(self, location, messages, time_delay=1):
"""
Send one or more messages to 'location' with a delay.

View file

@ -101,7 +101,7 @@ class Teapot(Object):
self.db.desc = f"A large, brown teapot full of {desc}."
drinker.msg(f"You make a teapot of {desc}.")
self.location.msg_contents(f"{drinker.name} makes a pot of tea.", exclude=drinker)
drinker.announce_action(f"makes a teapot of {desc}.")
def do_fill(self, drinker):
teatype = self.db.tea_type

View file

@ -168,13 +168,11 @@ class Fire(Pet):
if self.db.hunger_level < 5:
giver.msg(squish(f"You {get_up} put {adj} wood in the "
f"fireplace, and start a fire."))
self.location.msg_contents(squish(f"{giver.name} {gets_up} starts a fire."),
exclude=giver)
giver.announce_action(f"{gets_up} starts a fire.")
else:
giver.msg(squish(f"You {get_up} put {adj} wood on the "
f"fire in the fireplace."))
self.location.msg_contents(squish(f"{giver.name} {gets_up} puts {adj} wood on the fire."),
exclude=giver)
giver.announce_action(f"{gets_up} put wood on the fire.")
def feed(self, giver, obj=None):
"""

View file

@ -194,8 +194,7 @@ class Book(Readable):
def do_read(self, reader):
super().do_read(reader)
reader.location.msg_contents(routput(f"{reader.name} reads a book. You notice the title, |w{self.db.title}|n."),
exclude=reader)
reader.announce_action(f"reads |p book. You notice the title, |w{self.db.title}|n.")
def do_burn(self, reader, drop=False):
if isinstance(reader.location, DabblersRoom):
@ -204,7 +203,6 @@ class Book(Readable):
Gotta keep the room organized.""")
else:
reader.msg("You throw the book in the fireplace. It immediately yelps, flaps its pages, and flies back to a section on the shelf.")
self.location.msg_contents(routput(f"{reader.name} throws a book in the fireplace! It immediately yelps, flaps it pages, and flies back to a shelf."), exclude=reader)
self.delete()
else:
if not drop:

View file

@ -93,6 +93,7 @@ class Room(ObjectParent, ExtendedRoom, ContribRPRoom):
else:
return ''
class DabblersRoom(Room):
def get_display_desc(self, looker):
fire = self.search("fire")

View file

@ -40,7 +40,7 @@ TIMEBASE_WEATHER_MSGS = [
"The clouds part creating a dazzling sunrise of colors.",
"Bird song fills the air with the dawn chorus.",
# Cloudy
"The clouds accumulate making the morning last.",
"The clouds accumulate making the morning last longer.",
"The rain slows to a drizzle as the moss begins to glow in the brightening light.",
# Rainy
"The falling rain intensifies creating vibrating puddles.",
@ -137,7 +137,7 @@ class TimeWeatherRoom(Room):
even though we don't actually use them in this example)
"""
# only update 10 % of the time
if random.random() < 0.1:
if random.random() < 0.06:
msg = choose_weather_message()
if self.db.previous_weather != msg:
self.db.previous_weather = msg

View file

@ -45,8 +45,7 @@ class Sittable(Object):
self.db.sitter = sitter
sitter.db.is_sitting = self
sitter.msg(self.sit_msg())
self.location.msg_contents(f"{sitter.name} sits {adjective} {article} {self.key}.",
exclude=sitter)
sitter.announce_action(f"sits {adjective} {article} {self.key}.")
def do_stand(self, stander):
"""

View file

@ -125,8 +125,8 @@ class Pipe(Object):
you_msg = choices(self.db.light_msg or "You pack and light your pipe.")
lighter.msg(you_msg)
# desc = self.return_appearance()[:1].lower() + self.return_appearance()[1:]
other_msg = choices(self.db.light_msg_other or "{0} packs and lights |p pipe.", lighter.name)
lighter.location.msg_contents(other_msg, exclude=lighter)
other_msg = choices(self.db.light_msg_other or "packs and lights |p pipe.")
lighter.announce_action(other_msg)
def do_puff(self, smoker):
pass
@ -163,8 +163,7 @@ class Stick(Object):
"This is a <<fun ^ pleasant ^ nice>> <<past-time ^ game>>.",
"Maybe we should get a pet <<or beast ^ beastie ^ >> in here who would love to play.",
], thrower.name.capitalize()))
self.location.msg_contents(f"{thrower.name} throws a stick.",
exclude=thrower)
thrower.announce_action("throws |p stick.")
else:
thrower.msg("I think you should be outside or a place with more room before you throw that stick around.")
@ -198,10 +197,7 @@ class Puddle(Object):
"happy.",
"great.",
])))
self.location.msg_contents(f"{player.name} jumps in the puddle.",
exclude=player)
player.announce_action("jumps in the puddle.")
class Knocker_Convo(Enum):
@ -416,8 +412,7 @@ class Knocker(Object):
("room", self.db.room_to_msg)
])
knocker.msg("You grab the ring and knock firmly on the door.")
self.msg_contents(f"{knocker.name} grabs the ring and knocks firmly on the door.",
exclude=knocker)
knocker.announce_action("grabs the ring and knocks firmly on the door.")
else:
knocker.msg("This door knocker is defective, as it doesn't have a ring to...er, do the knockin'.")

View file

@ -49,23 +49,20 @@ MSGS = {
"\"How's it going?\" the bird asks, \"Are you enjoying this game so far?\"",
"The bird says, \"This place has been a respite from the outside turbulence.\"",
"\"A shortcut to |gsay|n is to type either a double or single quote, and then your message.\"",
"\"You can also use |gpose|n to state something about yourself, \" it says. \" For instance, type |gpose smiles.|n\"",
"\"You can also use |gemote|n to state something about yourself, \" it says. \" For instance, type |gemote smiles.|n\"",
# POSE:
"The bird stands on one leg.",
"\"See,\" says the bird, \"I s'pose I can pose too.\"",
"\"A shortcut to that command is typing |g:|n (a colon character).\"",
"The bird says, \"The game has more commands. Typing |ghelp|n gives you a list of commands. That list changes depending on where you are and what you are carrying. For instance, you could type |gshoo|n for me to fly away, and then that command wouldn't be available again.\"",
"Another useful command is |gpub|n which is a shortcut to sending messages to a public channel that everyone in the game receives |wout of character|n. That is, the message comes from your login account, not your character. The dad jokes go there without interrupting the role playing."
"The bird says, \"Typing |ghelp|n gives you a list of commands. That list changes depending on where you are and what you are carrying. For instance, you could type |gshoo|n for me to fly away, and then that command wouldn't be available again.\"",
# ],
# "HELP": [
"The bird chirps, \"You can learn more about the individual commands. For instance, type |ghelp say|n and you would see about the shortcuts I told you about. You can also see new commands, like |ghelp get|n.\"",
"The bird chirps, \"You can learn more about the individual commands. For instance, type |ghelp emote|n and you can see more options to that command. You can also see new commands, like |ghelp get|n.\"",
"\"Type |ghelp start|n for a repeat of much of what we've talked about,\" says the bird.",
"The bird says, \"The |ginv|n shows you what you are carrying. I see you picked up a letter. You can also type |gread letter|n.\"",
"\"Since this game hosts many users,\" says the bird, \"you can show them what they see when they look at you, using the |g@setdesc|n command. Check out the help, |ghelp @setdesc|n, or just type: |w@setdesc A pixie with a shock of blue hair and a gold chain.|n\"",
"\"Since this game hosts many users,\" says the bird, \"you can show them what they see when they look at you, using the |gsetdesc|n command. Check out the help on it and, |gsdesc|n and |gpose|n.",
"\"I'm guessing your last question,\" chirps the little bird on your shoulder, \"is the goal of this game.\"",
"\"That is a good question,\" says the bird, \"and I'm not sure what to say. This game has no goal, nor point. I guess you could look at it as a philosophical metaphor for existential existence in a post-modern world in the death throes of capitalism, but…\"",
"The bird says, \"This is a cozy little game about role playing a storybook character and maybe exploring and looking for eggs… no, that's not right. Something about eggs. Easter eggs? Something like that.\"",
"\"Wander and look around. Find a cozy place to chat with others who have logged in,\" says the bird.",
"\"If you're interested in |wbuilding|n and expanding this world,\" chirps the bird, \"talk to Dabbler... Oh, see if you can find Dabbler, the guy that made most of this. That is a great goal.\"",
"\"That is a good question,\" quips the bird, \"and I'm not sure what to say. You could look at this as a philosophical antidote for our existential apprehension of a post-modern world in the death throes of capitalism, but…\"",
"The bird says, \"This is a cozy little game about role playing a storybook character in an alternate plane populated by the Faerie, so wander and look around. Find a cozy place to role play with others,\" says the bird.",
"\"What do you say?\" says the bird. \"Think you got the hang of playing this?\"",
]
}
@ -107,8 +104,8 @@ class TutorBird(CarriableNPC):
"""
Bird that routinely tells a player a new hint.
"""
# The number of ticks to check for the time for a message:
bird_tick = 18
# The number of ticks (seconds) to check for the time for a message:
bird_tick = 15
# The number of waiting messages to issue before stopping the tutorial:
wait_messages = 3
@ -125,6 +122,7 @@ class TutorBird(CarriableNPC):
"SAY": 0,
}
self.db.waiting = 0
self.db.tutor_level = TutorialState.START
TICKER_HANDLER.add(interval=self.bird_tick,
callback=self.do_speak)
@ -138,14 +136,26 @@ class TutorBird(CarriableNPC):
})[0]
bird.location = character
def do_end_tutorial(self, character):
def do_speak(self):
"""
A visual way to stop the tutorial
Called at a repeatable sequence by the ticker, and
it calls at_say() in order to do a type of monologue.
"""
character.msg("\"Have fun, and I'll leave you here,\" chirps the bird, \"and remember to check out the letter you picked up!\" It flies away.")
self.delete()
character = self.location
msg = self.get_msg(character)
if msg:
character.msg(f"\n{msg}")
def get_msg_level(self, character, level):
def get_msg(self, character):
"""
Return a message based on the character's progression.
The progression is marked by the character's variable:
'tutorstate' (stored in its database).
"""
# We could skip ahead if the player enters a higher state, so
# we find the "max" state available:
level = TutorialState(max([character.db.tutorstate & state.value
for state in TutorialState]))
choices = MSGS[level.name]
msg_num = self.db.tutor_msg_num.get(level.name, 0)
if msg_num >= len(choices):
@ -161,23 +171,9 @@ class TutorBird(CarriableNPC):
self.db.waiting = 0 # Reset the waiting period
return msg
def get_msg(self, character, remaining_states):
try:
# Take the highest value from states...
level = remaining_states.pop()
if character.db.tutorstate >= level.value:
return self.get_msg_level(character, level)
else:
return self.get_msg(character, remaining_states)
except IndexError:
return self.get_msg_level(character, TutorialState.START)
def do_speak(self):
def do_end_tutorial(self, character):
"""
Called at a repeatable sequence by the ticker, and
it calls at_say() in order to do a type of monologue.
A visual way to stop the tutorial
"""
character = self.location
msg = self.get_msg(character, [level for level in TutorialState])
if msg:
character.msg(f"\n{msg}")
character.msg("\"Have fun, and I'll leave you here,\" chirps the bird. \"Remember to check out the letter you picked up!\" It flies away.")
self.delete()

View file

@ -4,7 +4,19 @@ from itertools import batched
from random import choice
from re import compile, sub, split
from evennia.utils import logger
from evennia.utils import logger, dedent
def paragraph(text):
"""
Removes initial spaces as well as final newlines.
Blank lines, however, are preserved.
"""
# Shame we can chain this or _thread_, but:
return sub(r";;", "\n\n",
sub(r"\n", " ",
sub(r"\n\n+", ";;",
dedent(text)))).strip()
def squish(text):
"Remove series of spaces from the text."

495
world/chargen_menu.py Normal file
View file

@ -0,0 +1,495 @@
"""
The WyldWood Character Creation
"""
import inflect
from typeclasses.characters import Character
# from evennia.prototypes.spawner import spawn
# from evennia.utils.evtable import EvTable
from utils.word_list import paragraph
_INFLECT = inflect.engine()
#########################################################
# Utilities
#########################################################
# 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)
#########################################################
# Welcome Page
#########################################################
def menunode_welcome(caller):
"""Starting page."""
char = caller.new_char
# another decision, so save the resume point
char.db.chargen_step = "menunode_welcome"
text = paragraph("""\
|wHoward's Character Creator, v1|n
This game takes place in the |cDomain of the WyldWood|n, and your
character may be a faery denizen of this strange wonderland, or
perhaps someone more typical, but who stepped into a faery circle
or other portal and landed in this strange, magical place.
To create this fantasy character, I will ask a series of
questions, and you will type your answer.
Type |ghelp|n at any of these prompts, for additional information.
Type |gquit|n to stop the process (recover your session using
|gcharcreate|n).
""") + """
1. Short Description
2. Set Gender
3. Character's "Pose"
4. Long Description
5. Character's Name
""" + "\nType |cReturn|n to begin creating your character."
help = paragraph("""
Type |gnext|n or |gskip|n (or even a blank answer), to keep any
previously entered data for the step and go to the next prompt.
Type |gback|n to return to a previous step, well, except for
this first message. Type |gquit|n instead. :-D
""")
options = {"key": "_default", "goto": _check_welcome}
return (text, help), options
def _check_welcome(caller, raw_string, **kwargs):
"""Check and fix the welcome message."""
return "menunode_sdesc"
#########################################################
# Short Description
#########################################################
def menunode_sdesc(caller, raw_string, **kwargs):
"""Starting page."""
char = caller.new_char
# another decision, so save the resume point
char.db.chargen_step = "menunode_sdesc"
# 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 short description:"
elif char.db._sdesc:
prompt_text = f"You previously wrote: |w{char.db._sdesc}|n"
else:
prompt_text = paragraph("""\
Let's begin with a two or three word description, e.g.
"big-eared hobbit", "beardless dwarf" or "frumpy, old woman":
""")
text = paragraph(f"""\
|wShort Description|n
Since this game emphasizes |wrole playing|n, we will pick our
character's name at the end (since no one will know, or should
know, your |wreal|n name).
When you and another player characters uses the |glook|n command,
you will see a description of the area, including a list of all
the other characters, but using a short description instead of the
character's name.
What should your |wshort description|n identify your character?
{prompt_text}""")
help = paragraph("""\
In fact, you will even use this description to refer to them, for
instance, to get a better description of the character, you might
type |glook hobbit|n, or if there are two hobbits in the area,
they would need to type more of
the description: |glook big-eared hobbit|n
|wNote:|n Type, |gnext|n to keep your value and skip to the next step.""")
# Free-formed text:
options = {"key": "_default", "goto": _check_sdesc}
return (text, help), options
def _check_sdesc(caller, raw_string, **kwargs):
"""Check and confirm short description"""
# strip any extraneous whitespace from the raw text
sdesc = raw_string.strip()
if sdesc == 'back':
return "menunode_welcome"
if sdesc in ('next', 'skip') or not sdesc:
return "menunode_gender"
# 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:
sdesc = caller.account.normalize_username(sdesc)
# Make sure it isn't too long.
if len(sdesc) > 30:
return (
"menunode_sdesc",
{"error": f"|w{sdesc}|n is too long. Try less than two dozen letters."},
)
caller.new_char.db._sdesc = sdesc
return "menunode_gender"
#########################################################
# Character Gender
#########################################################
def menunode_gender(caller, raw_string, **kwargs):
"""Choosing a gender for the character."""
char = caller.new_char
# another decision, so save the resume point
char.db.chargen_step = "menunode_gender"
text = paragraph(f"""\
|wCharacter Gender|n
Specifying the gender allows the system to use the correct
pronouns for your character. You can change this in the game
using the |ggender|n command.
Choose one of the following:""")
help = paragraph("""\
This feature only works from the system, not from other players.
A character would need to tell others of their preferred pronouns.
""")
options = (
{"key": "m",
"desc": "male with pronouns, he/him/his",
"goto": (_check_gender, {"gender": "male"}),
},
{"key": "f",
"desc": "female with pronouns, she/her/hers",
"goto": (_check_gender, {"gender": "female"}),
},
{"key": "n",
"desc": "neutral with pronouns, it/its",
"goto": (_check_gender, {"gender": "neutral"}),
},
{"key": "a",
"desc": "ambiguous with pronouns, they/them/their",
"goto": (_check_gender, {"gender": "ambiguous"}),
},
{"key": "b",
"desc": "go back and change the short description",
"goto": (menunode_sdesc),
},
)
return (text, help), options
def _check_gender(caller, rawstring, **kwargs):
"""Check and confirm pose"""
caller.new_char.db.gender = kwargs['gender']
return "menunode_pose"
#########################################################
# Posing the Character
#########################################################
def menunode_pose(caller, raw_string, **kwargs):
"""Posing a character."""
char = caller.new_char
# another decision, so save the resume point
char.db.chargen_step = "menunode_pose"
if char.db.pose:
prompt_text = f"You previously wrote: |w{char.db.pose}|n"
else:
prompt_text = ''
text = paragraph(f"""\
|wPosing a Character|n
Along with the short description given in the previous step, we
can |wpose|n our character. For instance, entering:
|wsmoking a pipe|n
Might show our character as:
a |mbig-eared hobbit|w smoking a pipe|n
As the game plays, you can change this at any time using the
|gpose|n command. This is just sets up a default pose, which can
return to with |gpose reset|n. Even this isn't permanent, as you
can change the default with something like:
|gpose default sitting on the ground|n
{prompt_text}
Enter an initial pose to describe, |m{char.db._sdesc}|n:""")
help = paragraph("""\
Don't worry much about this pose, since you can (and will) change
it as you play the game.
Having an initial pose makes the game (and your character) look
better.
Enter an initial pose to describe your character, or type |gback|n
to change your short description.""")
# Free-formed text:
options = {"key": "_default", "goto": _check_pose}
return (text, help), options
def _check_pose(caller, raw_string, **kwargs):
"""Check and confirm pose"""
# strip any extraneous whitespace from the raw text
pose = raw_string.strip()
if pose == 'back':
return "menunode_gender"
if pose in ('next', 'skip') or not pose:
return "menunode_desc"
# First, allow the character to be posed:
caller.new_char.pose = True
caller.new_char.db.pose = pose
caller.new_char.db.pose_default = pose
return "menunode_desc"
#########################################################
# Full Description
#########################################################
def menunode_desc(caller, raw_string, **kwargs):
"""Posing a character."""
char = caller.new_char
# another decision, so save the resume point
char.db.chargen_step = "menunode_desc"
if char.db.desc and char.db.desc != "This is a character.":
prompt_text = f"You previously wrote:\n\n |w{char.db.desc}|n"
else:
prompt_text = ''
text = paragraph(f"""
|wCharacter's Full Description|n
Unlike the short description given in the previous step (see only
when glancing around the room), a player will see this description
when they type:
|glook {char.db._sdesc}|n
This can be as long as you would like. Keep in mind this
description is what a player |wsees|n. so, no backstory here.
Well, unless you can cleverly weave in backstory hints from that
scar across your cheek.
You can change this at any time using the |gdesc|n command.
{prompt_text}
Enter your character's full description:""")
help = paragraph("""\
What makes a good character description? You want someone to be
able to picture your character in their mind, so hair, skin and
eye color, as well as what they are wearing.
To make your character truly memorable, mention the unique
features, so think of your character's backstory, and describe the
small hammer on your necklace, so you can tell others who inquire
about your father's last wish...
Enter your character's full description or type |gback|n to change
your pose.""")
# Free-formed text:
options = {"key": "_default", "goto": _check_desc}
return (text, help), options
def _check_desc(caller, raw_string, **kwargs):
"""Check and confirm desc"""
# strip any extraneous whitespace from the raw text
desc = raw_string.strip()
if desc == 'back':
return "menunode_pose"
if desc in ('next', 'skip') or not desc:
return "menunode_choose_name"
# First, allow the character to be descd:
caller.new_char.db.desc = desc
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 = paragraph(f"""\
|wChoosing a Name|n
Now, let's give your character a name. Unless you tell others, no
one will know (or need to know) your this name, however, you will
use this name, when you type |gic Name|n to choose this character
to play.
{prompt_text}""")
help = paragraph("""
Since you may have to type the entire name, you may want this to
be a single word for the first name. Your background could include
your full family name.
""")
# 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()
if charname == 'back':
return "menunode_desc"
if charname in ('next', 'skip') or not charname:
return (
"menunode_choose_name",
{"error": f"Can't skip this step. Please enter a name."},
)
# 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. Enter 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
text = paragraph(f"""\
|wCreate Character?|n
Shall we create your character, |c{char.key}|n:
|w{char.db._sdesc} {char.db.pose}|n
Description:
{char.db.desc}""")
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
# clear in-progress status
char.attributes.remove("chargen_step")
char.db.tutorstate = 0
char.db.visited = False
text = paragraph(f"""
You have created a character. I hope you enjoy playing this game.
To stop playing with this character, type: |gooc|n
You can create a new character with: |gcharcreate|n
To play with this one, type: |gic {caller.new_char.key}|n
As always, type |ghelp|n (especially |ghelp me start|n),
to learn more about how to play this game.
Alrighty then, let's log in and start playing the game.
Imagine that you are now in a strange land on the edge of Forever.
Welcome to the cozy forest section in the |wDomain of the WyldWood|n.
""")
return text, None