More refactoring

This commit is contained in:
Griatch 2022-09-02 08:10:39 +02:00
parent ba13e3e44f
commit 1f37f90bea
4 changed files with 323 additions and 324 deletions

View file

@ -382,7 +382,10 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
```
We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming
`race` as `charrace` thus matches in style.
`race` as `charrace` thus matches in style.
We'd then need to expand our [rules module](Beginner-Tutorial-Rules.md) (and later
[character generation](Beginner-Tutorial-Chargen.md) to check and include what these classes mean.
## Summary

View file

@ -5,9 +5,11 @@ Character class.
from evennia.objects.objects import DefaultCharacter
from evennia.typeclasses.attributes import AttributeProperty
from evennia.utils.evmenu import ask_yes_no
from evennia.utils.evform import EvForm
from evennia.utils.evmenu import EvMenu, ask_yes_no
from evennia.utils.evtable import EvTable
from evennia.utils.logger import log_trace
from evennia.utils.utils import lazy_property
from evennia.utils.utils import inherits_from, lazy_property
from . import rules
from .equipment import EquipmentError, EquipmentHandler
@ -165,9 +167,11 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
hp = AttributeProperty(default=4)
hp_max = AttributeProperty(default=4)
level = AttributeProperty(default=1)
xp = AttributeProperty(default=0)
coins = AttributeProperty(default=0) # copper coins
xp = AttributeProperty(default=0)
xp_per_level = 1000
@lazy_property
def equipment(self):
"""Allows to access equipment like char.equipment.worn"""
@ -275,3 +279,135 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
"""
pass
def add_xp(self, xp):
"""
Add new XP.
Args:
xp (int): The amount of gained XP.
Returns:
bool: If a new level was reached or not.
Notes:
level 1 -> 2 = 1000 XP
level 2 -> 3 = 2000 XP etc
"""
self.xp += xp
next_level_xp = self.level * self.xp_per_level
return self.xp >= next_level_xp
def level_up(self, *abilities):
"""
Perform the level-up action.
Args:
*abilities (str): A set of abilities (like 'strength', 'dexterity' (normally 3)
to upgrade by 1. Max is usually +10.
Notes:
We block increases above a certain value, but we don't raise an error here, that
will need to be done earlier, when the user selects the ability to increase.
"""
self.level += 1
for ability in set(abilities[:3]):
# limit to max amount allowed, each one unique
try:
# set at most to the max bonus
current_bonus = getattr(self, ability)
setattr(
self,
ability,
min(10, current_bonus + 1),
)
except AttributeError:
pass
# update hp
self.hp_max = max(self.max_hp + 1, rules.dice.roll(f"{self.level}d8"))
# character sheet visualization
_SHEET = """
+----------------------------------------------------------------------------+
| Name: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
+----------------------------------------------------------------------------+
| STR: x2xxxxx DEX: x3xxxxx CON: x4xxxxx WIS: x5xxxxx CHA: x6xxxxx |
+----------------------------------------------------------------------------+
| HP: x7xxxxx XP: x8xxxxx Level: x9x |
+----------------------------------------------------------------------------+
| Desc: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
+----------------------------------------------------------------------------+
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccc1ccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
+----------------------------------------------------------------------------+
"""
def get_character_sheet(data):
"""
Generate a character sheet. This is grouped in a class in order to make
it easier to override the look of the sheet.
Args:
data (EvAdventureCharacter or EvAdventureCharacterGeneration): This contains
the data to put in the sheet, either as stored on a finished character
or on the temporary chargen storage object.
"""
@staticmethod
def get(data):
"""
Generate a character sheet from the character's stats.
data
"""
if inherits_from(data, DefaultCharacter):
# a character, get info normally
equipment = [item.key for item in character.equipment.all()]
else:
equipment
# divide into chunks of max 10 length (to go into two columns)
equipment_table = EvTable(
table=[equipment[i : i + 10] for i in range(0, len(equipment), 10)]
)
form = EvForm({"FORMCHAR": "x", "TABLECHAR": "c", "SHEET": _SHEET})
form.map(
cells={
1: character.key,
2: f"+{data.strength}({data.strength + 10})",
3: f"+{data.dexterity}({data.dexterity + 10})",
4: f"+{data.constitution}({data.constitution + 10})",
5: f"+{data.wisdom}({data.wisdom + 10})",
6: f"+{data.charisma}({data.charisma + 10})",
7: f"{data.hp}/{data.hp_max}",
8: data.xp,
9: data.level,
"A": data.db.desc,
},
tables={
1: equipment_table,
},
)
return str(form)

View file

@ -0,0 +1,180 @@
"""
EvAdventure character generation
"""
from evennia.prototypes.spawner import spawn
from evennia.utils.evmenu import EvMenu
from .random_tables import chargen_table
from .rules import dice
class EvAdventureCharacterGeneration:
"""
This collects all the rules for generating a new character. An instance of this class can be
used to track all the stats during generation and will be used to apply all the data 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"))
def generate(self):
"""
Generate random values for character.
"""
# name will likely be modified later
self.name = dice.roll_random_table("1d282", chargen_table["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)
self.physique = dice.roll_random_table("1d20", chargen_table["physique"])
self.face = dice.roll_random_table("1d20", chargen_table["face"])
self.skin = dice.roll_random_table("1d20", chargen_table["skin"])
self.hair = dice.roll_random_table("1d20", chargen_table["hair"])
self.clothing = dice.roll_random_table("1d20", chargen_table["clothing"])
self.speech = dice.roll_random_table("1d20", chargen_table["speech"])
self.virtue = dice.roll_random_table("1d20", chargen_table["virtue"])
self.vice = dice.roll_random_table("1d20", chargen_table["vice"])
self.background = dice.roll_random_table("1d20", chargen_table["background"])
self.misfortune = dice.roll_random_table("1d20", chargen_table["misfortune"])
self.alignment = dice.roll_random_table("1d20", chargen_table["alignment"])
# same for all
self.exploration_speed = 120
self.combat_speed = 40
self.hp_max = max(5, dice.roll("1d8"))
self.hp = self.hp_max
self.xp = 0
self.level = 1
# random equipment
self.armor = dice.roll_random_table("1d20", chargen_table["armor"])
_helmet_and_shield = dice.roll_random_table("1d20", chargen_table["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_table["starting weapon"])
self.backpack = [
"ration",
"ration",
dice.roll_random_table("1d20", chargen_table["dungeoning gear"]),
dice.roll_random_table("1d20", chargen_table["dungeoning gear"]),
dice.roll_random_table("1d20", chargen_table["general gear 1"]),
dice.roll_random_table("1d20", chargen_table["general gear 2"]),
]
def build_desc(self):
"""
Generate a backstory / description paragraph from random elements.
"""
return (
f"{self.background.title()}. Wears {self.clothing} clothes, and has {self.speech} "
f"speech. Has a {self.physique} physique, a {self.face} face, {self.skin} skin and "
f"{self.hair} hair. Is {self.virtue}, but {self.vice}. Has been {self.misfortune} in "
f"the past. Favors {self.alignment}."
)
def show_sheet(self):
return get_character_sheet(self)
def adjust_attribute(self, source_attribute, target_attribute, value):
"""
Redistribute bonus from one attribute to another. The resulting values
must not be lower than +1 and not above +6.
Args:
source_attribute (enum.Ability): The name of the attribute to deduct bonus from,
like 'strength'
target_attribute (str): The attribute to give the bonus to, like 'dexterity'.
value (int): How much to change. This is always 1 for the current chargen.
Raises:
ValueError: On input error, using invalid values etc.
Notes:
We assume the strings are provided by the chargen, so we don't do
much input validation here, we do make sure we don't overcharge ourselves though.
"""
if source_attribute == target_attribute:
return
# we use getattr() to fetch the Ability of e.g. the .strength property etc
source_current = getattr(self, source_attribute.value, 1)
target_current = getattr(self, target_attribute.value, 1)
if source_current - value < 1:
raise ValueError(f"You can't reduce the {source_attribute} bonus below +1.")
if target_current + value > 6:
raise ValueError(f"You can't increase the {target_attribute} bonus above +6.")
# all is good, apply the change.
setattr(self, source_attribute.value, source_current - value)
setattr(self, target_attribute.value, target_current + value)
def apply(self, character):
"""
Once the chargen is complete, call this to transfer all the data to the character
permanently.
"""
character.key = self.name
character.strength = self.strength
character.dexterity = self.dexterity
character.constitution = self.constitution
character.intelligence = self.intelligence
character.wisdom = self.wisdom
character.charisma = self.charisma
character.hp = self.hp
character.level = self.level
character.xp = self.xp
character.db.desc = self.build_desc()
if self.weapon:
weapon = spawn(self.weapon)
character.equipment.move(weapon)
if self.shield:
shield = spawn(self.shield)
character.equipment.move(shield)
if self.armor:
armor = spawn(self.armor)
character.equipment.move(armor)
if self.helmet:
helmet = spawn(self.helmet)
character.equipment.move(helmet)
for item in self.backpack:
item = spawn(item)
character.equipment.store(item)
# chargen menu

View file

@ -344,327 +344,7 @@ class EvAdventureRollEngine:
)
# character generation
class EvAdventureCharacterGeneration:
"""
This collects all the rules for generating a new character. An instance of this class can be
used to track all the stats during generation and will be used to apply all the data 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:
Unlike standard Knave, characters will come out more similar here. This is because in
a table top game it's fun to roll randomly and have to live with a crappy roll - but
online players can (and usually will) just disconnect and reroll until they get values
they are happy with.
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. Instead we
homogenize the results to a flat +2 bonus and let people redistribute the
points afterwards. This also allows us to show off some more advanced concepts in the
chargen menu.
In the same way, Knave uses a d8 roll to get the initial hit points. Instead we use a
flat max of 8 HP to start, in order to give players a little more survivability.
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 __init__(self):
"""
Initialize starting values
"""
# for clarity we initialize the engine here rather than use the
# global singleton at the end of the module
roll_engine = EvAdventureRollEngine()
# name will likely be modified later
self.name = roll_engine.roll_random_table("1d282", chargen_table["name"])
# base attribute bonuses (flat +1 bonus)
self.strength = 2
self.dexterity = 2
self.constitution = 2
self.intelligence = 2
self.wisdom = 2
self.charisma = 2
# physical attributes (only for rp purposes)
self.physique = roll_engine.roll_random_table("1d20", chargen_table["physique"])
self.face = roll_engine.roll_random_table("1d20", chargen_table["face"])
self.skin = roll_engine.roll_random_table("1d20", chargen_table["skin"])
self.hair = roll_engine.roll_random_table("1d20", chargen_table["hair"])
self.clothing = roll_engine.roll_random_table("1d20", chargen_table["clothing"])
self.speech = roll_engine.roll_random_table("1d20", chargen_table["speech"])
self.virtue = roll_engine.roll_random_table("1d20", chargen_table["virtue"])
self.vice = roll_engine.roll_random_table("1d20", chargen_table["vice"])
self.background = roll_engine.roll_random_table("1d20", chargen_table["background"])
self.misfortune = roll_engine.roll_random_table("1d20", chargen_table["misfortune"])
self.alignment = roll_engine.roll_random_table("1d20", chargen_table["alignment"])
# same for all
self.exploration_speed = 120
self.combat_speed = 40
self.hp_max = 8
self.hp = self.hp_max
self.xp = 0
self.level = 1
# random equipment
self.armor = roll_engine.roll_random_table("1d20", chargen_table["armor"])
_helmet_and_shield = roll_engine.roll_random_table(
"1d20", chargen_table["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 = roll_engine.roll_random_table("1d20", chargen_table["starting weapon"])
self.backpack = [
"ration",
"ration",
roll_engine.roll_random_table("1d20", chargen_table["dungeoning gear"]),
roll_engine.roll_random_table("1d20", chargen_table["dungeoning gear"]),
roll_engine.roll_random_table("1d20", chargen_table["general gear 1"]),
roll_engine.roll_random_table("1d20", chargen_table["general gear 2"]),
]
def build_desc(self):
"""
Generate a backstory / description paragraph from random elements.
"""
return (
f"{self.background.title()}. Wears {self.clothing} clothes, and has {self.speech} "
f"speech. Has a {self.physique} physique, a {self.face} face, {self.skin} skin and "
f"{self.hair} hair. Is {self.virtue}, but {self.vice}. Has been {self.misfortune} in "
f"the past. Favors {self.alignment}."
)
def adjust_attribute(self, source_attribute, target_attribute, value):
"""
Redistribute bonus from one attribute to another. The resulting values
must not be lower than +1 and not above +6.
Args:
source_attribute (enum.Ability): The name of the attribute to deduct bonus from,
like 'strength'
target_attribute (str): The attribute to give the bonus to, like 'dexterity'.
value (int): How much to change. This is always 1 for the current chargen.
Raises:
ValueError: On input error, using invalid values etc.
Notes:
We assume the strings are provided by the chargen, so we don't do
much input validation here, we do make sure we don't overcharge ourselves though.
"""
if source_attribute == target_attribute:
return
# we use getattr() to fetch the Ability of e.g. the .strength property etc
source_current = getattr(self, source_attribute.value, 1)
target_current = getattr(self, target_attribute.value, 1)
if source_current - value < 1:
raise ValueError(f"You can't reduce the {source_attribute} bonus below +1.")
if target_current + value > 6:
raise ValueError(f"You can't increase the {target_attribute} bonus above +6.")
# all is good, apply the change.
setattr(self, source_attribute.value, source_current - value)
setattr(self, target_attribute.value, target_current + value)
def apply(self, character):
"""
Once the chargen is complete, call this to transfer all the data to the character
permanently.
"""
character.key = self.name
character.strength = self.strength
character.dexterity = self.dexterity
character.constitution = self.constitution
character.intelligence = self.intelligence
character.wisdom = self.wisdom
character.charisma = self.charisma
character.weapon = self.weapon
character.armor = self.armor
character.hp = self.hp
character.level = self.level
character.xp = self.xp
character.db.desc = self.build_desc()
# TODO - spawn the actual equipment objects before adding them to equipment!
if self.weapon:
character.equipment.use(self.weapon)
if self.shield:
character.equipment.use(self.shield)
if self.armor:
character.equipment.use(self.armor)
if self.helmet:
character.equipment.use(self.helmet)
for item in self.backpack:
# TODO create here
character.equipment.store(item)
# character improvement
class EvAdventureImprovement:
"""
Handle XP gains and level upgrades. Grouped in a class in order to
make it easier to override the mechanism.
"""
xp_per_level = 1000
amount_of_abilities_to_upgrade = 3
max_ability_bonus = 10 # bonus +10, defense 20
@staticmethod
def add_xp(character, xp):
"""
Add new XP.
Args:
character (Character): The character to improve.
xp (int): The amount of gained XP.
Returns:
bool: If a new level was reached or not.
Notes:
level 1 -> 2 = 1000 XP
level 2 -> 3 = 2000 XP etc
"""
character.xp += xp
next_level_xp = character.level * EvAdventureImprovement.xp_per_level
return character.xp >= next_level_xp
@staticmethod
def level_up(character, *abilities):
"""
Perform the level-up action.
Args:
character (Character): The entity to level-up.
*abilities (str): A set of abilities (like 'strength', 'dexterity' (normally 3)
to upgrade by 1. Max is usually +10.
Notes:
We block increases above a certain value, but we don't raise an error here, that
will need to be done earlier, when the user selects the ability to increase.
"""
roll_engine = EvAdventureRollEngine()
character.level += 1
for ability in set(abilities[: EvAdventureImprovement.amount_of_abilities_to_upgrades]):
# limit to max amount allowed, each one unique
try:
# set at most to the max bonus
current_bonus = getattr(character, ability)
setattr(
character,
ability,
min(EvAdventureImprovement.max_ability_bonus, current_bonus + 1),
)
except AttributeError:
pass
character.hp_max = max(character.max_hp + 1, roll_engine.roll(f"{character.level}d8"))
# character sheet visualization
_SHEET = """
+----------------------------------------------------------------------------+
| Name: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
+----------------------------------------------------------------------------+
| STR: x2xxxxx DEX: x3xxxxx CON: x4xxxxx WIS: x5xxxxx CHA: x6xxxxx |
+----------------------------------------------------------------------------+
| HP: x7xxxxx XP: x8xxxxx Exploration speed: x9x Combat speed: xAx |
+----------------------------------------------------------------------------+
| Desc: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
+----------------------------------------------------------------------------+
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccc1ccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
+----------------------------------------------------------------------------+
"""
def get_character_sheet(character):
"""
Generate a character sheet. This is grouped in a class in order to make
it easier to override the look of the sheet.
"""
@staticmethod
def get(character):
"""
Generate a character sheet from the character's stats.
"""
equipment = character.equipment.wielded + character.equipment.worn + character.carried
# divide into chunks of max 10 length (to go into two columns)
equipment_table = EvTable(
table=[equipment[i : i + 10] for i in range(0, len(equipment), 10)]
)
form = EvForm({"FORMCHAR": "x", "TABLECHAR": "c", "SHEET": _SHEET})
form.map(
cells={
1: character.key,
2: f"+{character.strength}({character.strength + 10})",
3: f"+{character.dexterity}({character.dexterity + 10})",
4: f"+{character.constitution}({character.constitution + 10})",
5: f"+{character.wisdom}({character.wisdom + 10})",
6: f"+{character.charisma}({character.charisma + 10})",
7: f"{character.hp}/{character.hp_max}",
8: character.xp,
9: character.exploration_speed,
"A": character.combat_speed,
"B": character.db.desc,
},
tables={
1: equipment_table,
},
)
return str(form)
# singletons
# access rolls e.g. with rules.dice.opposed_saving_throw(...)
dice = EvAdventureRollEngine()
# access improvement e.g. with rules.improvement.add_xp(character, xp)
improvement = EvAdventureImprovement()