diff --git a/commands/default_cmdsets.py b/commands/default_cmdsets.py index ed4e533..e6ecb75 100644 --- a/commands/default_cmdsets.py +++ b/commands/default_cmdsets.py @@ -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): diff --git a/commands/everyone.py b/commands/everyone.py index 19f44bf..4cf9d44 100755 --- a/commands/everyone.py +++ b/commands/everyone.py @@ -9,30 +9,71 @@ 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 = + whisper , = + + 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 = ") + 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: - - 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: + - yell : To replace 'says' with 'yells' + - scream : To replace 'says' with 'screams' + - ask : To replace 'says' with 'asks' + - 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. + say/quietly Hi there. - Shows as: + Shows as: - You quietly say, "Hi there." + You quietly say, "Hi there." """ key = "say" @@ -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) diff --git a/server/conf/settings.py b/server/conf/settings.py index d545838..1651086 100644 --- a/server/conf/settings.py +++ b/server/conf/settings.py @@ -53,6 +53,11 @@ FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = True SEARCH_MULTIMATCH_REGEX = r"(?P[0-9]+)-(?P[^-]*)(?P.*)" 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. ###################################################################### diff --git a/typeclasses/accounts.py b/typeclasses/accounts.py index 12650e2..ddac284 100644 --- a/typeclasses/accounts.py +++ b/typeclasses/accounts.py @@ -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): diff --git a/typeclasses/characters.py b/typeclasses/characters.py index bf79766..4b619a9 100644 --- a/typeclasses/characters.py +++ b/typeclasses/characters.py @@ -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. diff --git a/typeclasses/drinkables.py b/typeclasses/drinkables.py index 057351f..e8757c1 100755 --- a/typeclasses/drinkables.py +++ b/typeclasses/drinkables.py @@ -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 diff --git a/typeclasses/pets.py b/typeclasses/pets.py index bc5cdf0..41f9306 100755 --- a/typeclasses/pets.py +++ b/typeclasses/pets.py @@ -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): """ diff --git a/typeclasses/readables.py b/typeclasses/readables.py index a4963bb..3aa7104 100755 --- a/typeclasses/readables.py +++ b/typeclasses/readables.py @@ -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: diff --git a/typeclasses/rooms.py b/typeclasses/rooms.py index 3eb25fd..bf030e1 100644 --- a/typeclasses/rooms.py +++ b/typeclasses/rooms.py @@ -93,6 +93,7 @@ class Room(ObjectParent, ExtendedRoom, ContribRPRoom): else: return '' + class DabblersRoom(Room): def get_display_desc(self, looker): fire = self.search("fire") diff --git a/typeclasses/rooms_weather.py b/typeclasses/rooms_weather.py index bea59fc..1a9cad2 100755 --- a/typeclasses/rooms_weather.py +++ b/typeclasses/rooms_weather.py @@ -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 diff --git a/typeclasses/sittables.py b/typeclasses/sittables.py index be6886c..e6f9cba 100755 --- a/typeclasses/sittables.py +++ b/typeclasses/sittables.py @@ -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): """ diff --git a/typeclasses/things.py b/typeclasses/things.py index b8db0be..9e5ee39 100755 --- a/typeclasses/things.py +++ b/typeclasses/things.py @@ -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 <> <>.", "Maybe we should get a pet <> 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'.") diff --git a/typeclasses/tutorial.py b/typeclasses/tutorial.py index 02ea708..1ab204b 100755 --- a/typeclasses/tutorial.py +++ b/typeclasses/tutorial.py @@ -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.\"", - "\"Type |ghelp start|n for a repeat of much of what we've talked about,\" says the bird.", + "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() diff --git a/utils/word_list.py b/utils/word_list.py index ae2f5c3..04c9128 100755 --- a/utils/word_list.py +++ b/utils/word_list.py @@ -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." diff --git a/world/chargen_menu.py b/world/chargen_menu.py new file mode 100644 index 0000000..05b2c38 --- /dev/null +++ b/world/chargen_menu.py @@ -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