"""
EvAdventure character generation.
"""
from django.conf import settings
from evennia.objects.models import ObjectDB
from evennia.prototypes.spawner import spawn
from evennia.utils.create import create_object
from evennia.utils.evmenu import EvMenu
from .characters import EvAdventureCharacter
from .random_tables import chargen_tables
from .rules import dice
_ABILITIES = {
"STR": "strength",
"DEX": "dexterity",
"CON": "constitution",
"INT": "intelligence",
"WIS": "wisdom",
"CHA": "charisma",
}
_TEMP_SHEET = """
{name}
STR +{strength}
DEX +{dexterity}
CON +{constitution}
INT +{intelligence}
WIS +{wisdom}
CHA +{charisma}
{description}
Your belongings:
{equipment}
"""
[docs]class TemporaryCharacterSheet:
"""
This collects all the rules for generating a new character. An instance of this class is used
to pass around the current character state during character generation and also applied to
the character at the end. This class instance can also be saved on the menu to make sure a user
is not losing their half-created character.
Note:
In standard Knave, the character's attribute bonus is rolled randomly and will give a
value 1-6; and there is no guarantee for 'equal' starting characters.
Knave uses a d8 roll to get the initial hit points. We will follow the recommendation
from the rule that we will use a minimum of 5 HP.
We *will* roll random start equipment though. Contrary to standard Knave, we'll also
randomly assign the starting weapon among a small selection of equal-dmg weapons (since
there is no GM to adjudicate a different choice).
"""
def _random_ability(self):
return min(dice.roll("1d6"), dice.roll("1d6"), dice.roll("1d6"))
[docs] def __init__(self):
# name will likely be modified later
self.name = dice.roll_random_table("1d282", chargen_tables["name"])
# base attribute values
self.strength = self._random_ability()
self.dexterity = self._random_ability()
self.constitution = self._random_ability()
self.intelligence = self._random_ability()
self.wisdom = self._random_ability()
self.charisma = self._random_ability()
# physical attributes (only for rp purposes)
physique = dice.roll_random_table("1d20", chargen_tables["physique"])
face = dice.roll_random_table("1d20", chargen_tables["face"])
skin = dice.roll_random_table("1d20", chargen_tables["skin"])
hair = dice.roll_random_table("1d20", chargen_tables["hair"])
clothing = dice.roll_random_table("1d20", chargen_tables["clothing"])
speech = dice.roll_random_table("1d20", chargen_tables["speech"])
virtue = dice.roll_random_table("1d20", chargen_tables["virtue"])
vice = dice.roll_random_table("1d20", chargen_tables["vice"])
background = dice.roll_random_table("1d20", chargen_tables["background"])
misfortune = dice.roll_random_table("1d20", chargen_tables["misfortune"])
alignment = dice.roll_random_table("1d20", chargen_tables["alignment"])
self.ability_changes = 0
self.desc = (
f"You are {physique} with a {face} face, {skin} skin, {hair} hair, {speech} speech, and"
f" {clothing} clothing. You were a {background.title()}, but you were {misfortune} and"
f" ended up a knave. You are {virtue} but also {vice}. You tend towards {alignment}."
)
# same for all
self.hp_max = max(5, dice.roll("1d8"))
self.hp = self.hp_max
# random equipment
self.armor = dice.roll_random_table("1d20", chargen_tables["armor"])
_helmet_and_shield = dice.roll_random_table("1d20", chargen_tables["helmets and shields"])
self.helmet = "helmet" if "helmet" in _helmet_and_shield else "none"
self.shield = "shield" if "shield" in _helmet_and_shield else "none"
self.weapon = dice.roll_random_table("1d20", chargen_tables["starting weapon"])
self.backpack = [
"ration",
"ration",
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
dice.roll_random_table("1d20", chargen_tables["general gear 1"]),
dice.roll_random_table("1d20", chargen_tables["general gear 2"]),
]
[docs] def show_sheet(self):
"""
Show a temp character sheet, a compressed version of the real thing.
"""
equipment = (
str(item)
for item in [self.armor, self.helmet, self.shield, self.weapon] + self.backpack
if item
)
return _TEMP_SHEET.format(
name=self.name,
strength=self.strength,
dexterity=self.dexterity,
constitution=self.constitution,
intelligence=self.intelligence,
wisdom=self.wisdom,
charisma=self.charisma,
description=self.desc,
equipment=", ".join(equipment),
)
[docs] def apply(self, account):
"""
Once the chargen is complete, call this create and set up the character.
"""
start_location = ObjectDB.objects.get_id(settings.START_LOCATION)
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
# creating character with given abilities
new_character = create_object(
EvAdventureCharacter,
key=self.name,
location=start_location,
home=default_home,
permissions=permissions,
attributes=(
("strength", self.strength),
("dexterity", self.dexterity),
("constitution", self.constitution),
("intelligence", self.intelligence),
("wisdom", self.wisdom),
("charisma", self.wisdom),
("hp", self.hp),
("hp_max", self.hp_max),
("desc", self.desc),
),
)
new_character.locks.add(
"puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer);delete:id(%i) or"
" perm(Admin)" % (new_character.id, account.id, account.id)
)
# spawn equipment
if self.weapon:
weapon = spawn(self.weapon)
new_character.equipment.move(weapon[0])
if self.armor:
armor = spawn(self.armor)
new_character.equipment.move(armor[0])
if self.shield:
shield = spawn(self.shield)
new_character.equipment.move(shield[0])
if self.helmet:
helmet = spawn(self.helmet)
new_character.equipment.move(helmet[0])
for item in self.backpack:
item = spawn(item)
new_character.equipment.move(item[0])
return new_character
# chargen menu
[docs]def node_chargen(caller, raw_string, **kwargs):
"""
This node is the central point of chargen. We return here to see our current
sheet and break off to edit different parts of it.
In Knave, not so much can be changed.
"""
tmp_character = kwargs["tmp_character"]
text = tmp_character.show_sheet()
options = [{"desc": "Change your name", "goto": ("node_change_name", kwargs)}]
if tmp_character.ability_changes <= 0:
options.append(
{
"desc": "Swap two of your ability scores (once)",
"goto": ("node_swap_abilities", kwargs),
}
)
options.append(
{"desc": "Accept and create character", "goto": ("node_apply_character", kwargs)},
)
return text, options
def _update_name(caller, raw_string, **kwargs):
"""
Used by node_change_name below to check what user entered and update the name if appropriate.
"""
if raw_string:
tmp_character = kwargs["tmp_character"]
tmp_character.name = raw_string.lower().capitalize()
return "node_chargen", kwargs
[docs]def node_change_name(caller, raw_string, **kwargs):
"""
Change the random name of the character.
"""
tmp_character = kwargs["tmp_character"]
text = (
f"Your current name is |w{tmp_character.name}|n. Enter a new name or leave empty to abort."
)
options = {"key": "_default", "goto": (_update_name, kwargs)}
return text, options
def _swap_abilities(caller, raw_string, **kwargs):
"""
Used by node_swap_abilities to parse the user's input and swap ability
values.
"""
if raw_string:
abi1, *abi2 = raw_string.split(" ", 1)
if not abi2:
caller.msg("That doesn't look right.")
return None, kwargs
abi2 = abi2[0]
abi1, abi2 = abi1.upper().strip(), abi2.upper().strip()
if abi1 not in _ABILITIES or abi2 not in _ABILITIES:
caller.msg("Not a familiar set of abilites.")
return None, kwargs
# looks okay = swap values. We need to convert STR to strength etc
tmp_character = kwargs["tmp_character"]
abi1 = _ABILITIES[abi1]
abi2 = _ABILITIES[abi2]
abival1 = getattr(tmp_character, abi1)
abival2 = getattr(tmp_character, abi2)
setattr(tmp_character, abi1, abival2)
setattr(tmp_character, abi2, abival1)
tmp_character.ability_changes += 1
return "node_chargen", kwargs
[docs]def node_swap_abilities(caller, raw_string, **kwargs):
"""
One is allowed to swap the values of two abilities around, once.
"""
tmp_character = kwargs["tmp_character"]
text = f"""
Your current abilities:
STR +{tmp_character.strength}
DEX +{tmp_character.dexterity}
CON +{tmp_character.constitution}
INT +{tmp_character.intelligence}
WIS +{tmp_character.wisdom}
CHA +{tmp_character.charisma}
You can swap the values of two abilities around.
You can only do this once, so choose carefully!
To swap the values of e.g. STR and INT, write |wSTR INT|n. Empty to abort.
"""
options = {"key": "_default", "goto": (_swap_abilities, kwargs)}
return text, options
[docs]def node_apply_character(caller, raw_string, **kwargs):
"""
End chargen and create the character. We will also puppet it.
"""
tmp_character = kwargs["tmp_character"]
new_character = tmp_character.apply(caller)
caller.characters.add(new_character)
text = "Character created!"
return text, None
[docs]def start_chargen(caller, session=None):
"""
This is a start point for spinning up the chargen from a command later.
"""
menutree = {
"node_chargen": node_chargen,
"node_change_name": node_change_name,
"node_swap_abilities": node_swap_abilities,
"node_apply_character": node_apply_character,
}
# this generates all random components of the character
tmp_character = TemporaryCharacterSheet()
EvMenu(
caller,
menutree,
startnode="node_chargen",
session=session,
startnode_input=("sgsg", {"tmp_character": tmp_character}),
)