Added basic support for Races and some raw text boxes.

This commit is contained in:
Mark Wolfman
2018-03-28 19:52:59 -05:00
parent e44bb9203b
commit cafde3465d
8 changed files with 278 additions and 27 deletions
+6
View File
@@ -1,3 +1,9 @@
# Emacs temp files
*~
# Pytest
.pytest_cache/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
+53 -19
View File
@@ -1,10 +1,12 @@
"""Tools for describing a player character."""
import re
import warnings
from .stats import Ability, Skill, findattr
from .dice import read_dice_str
from . import weapons
from . import weapons, race
from .weapons import Weapon
dice_re = re.compile('(\d+)d(\d+)')
@@ -20,9 +22,8 @@ class Character():
background = ""
level = 1
alignment = "Neutral"
race = "Human"
race = None
xp = 0
speed = 30 # In feet
# Hit points
hp_max = 10
hit_dice_faces = 2
@@ -35,7 +36,7 @@ class Character():
charisma = Ability()
saving_throw_proficiencies = []
skill_proficiencies = tuple()
weapon_proficienies = tuple()
weapon_proficiencies = tuple()
proficiencies_extra = tuple()
languages = ""
# Skills
@@ -58,10 +59,12 @@ class Character():
stealth = Skill(ability='dexterity')
survival = Skill(ability='wisdom')
# Characteristics
attacks_and_spellcasting = ""
personality_traits = ""
ideals = ""
bonds = ""
flaws = ""
features_and_traits = ""
# Inventory
cp = 0
sp = 0
@@ -77,6 +80,16 @@ class Character():
self.weapons = []
self.set_attrs(**attrs)
def __str__(self):
return self.name
def __repr__(self):
return f"<{self.class_name}: {self.name}>"
@property
def speed(self):
return self.race.speed
def set_attrs(self, **attrs):
"""Bulk setting of attributes. Useful for loading a character from a
dictionary."""
@@ -85,6 +98,9 @@ class Character():
# Treat weapons specially
for weap in val:
self.wield_weapon(weap)
elif attr == 'race':
MyRace = findattr(race, val)
self.race = MyRace()
else:
if not hasattr(self, attr):
warnings.warn(f"Setting unknown character attribute {attr}",
@@ -92,10 +108,29 @@ class Character():
# Lookup general attributes
setattr(self, attr, val)
def is_proficient(self, weapon: Weapon):
"""Is the character proficient with this item?
Considers class proficiencies and race proficiencies.
Parameters
----------
weapon
The weapon to be tested for proficiency.
"""
all_proficiencies = tuple(self.weapon_proficiencies)
all_proficiencies += tuple(getattr(self.race, 'weapon_proficiencies', tuple()))
is_proficient = any((isinstance(weapon, W) for W in all_proficiencies))
return is_proficient
@property
def proficiencies_text(self):
final_text = ""
all_proficiencies = (self._proficiencies_text + self.proficiencies_extra)
all_proficiencies = self._proficiencies_text
if self.race is not None:
all_proficiencies += self.race.proficiencies_text
all_proficiencies += self.proficiencies_extra
# Create a single string out of all the proficiencies
for txt in all_proficiencies:
if not final_text:
@@ -133,8 +168,7 @@ class Character():
weapon_.attack_bonus += ability_mod
weapon_.bonus_damage += ability_mod
# Check for prifiency
is_proficient = (weapon_.__class__ in self.weapon_proficienies)
if is_proficient:
if self.is_proficient(weapon_):
weapon_.attack_bonus += self.proficiency_bonus
# Save it to the array
self.weapons.append(weapon_)
@@ -172,7 +206,7 @@ class Barbarian(Character):
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'simple weapons', 'martial weapons')
weapon_proficienies = (weapons.simple_weapons + weapons.martial_weapons)
weapon_proficiencies = (weapons.simple_weapons + weapons.martial_weapons)
class Bard(Character):
@@ -182,7 +216,7 @@ class Bard(Character):
_proficiencies_text = (
'Light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', 'three musical instruments of your choice')
weapon_proficienies = ((weapons.HandCrossbow, weapons.Longsword,
weapon_proficiencies = ((weapons.HandCrossbow, weapons.Longsword,
weapons.Rapier, weapons.Shortsword) +
weapons.simple_weapons)
@@ -193,7 +227,7 @@ class Cleric(Character):
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'all simple weapons')
weapon_proficienies = weapons.simple_weapons
weapon_proficiencies = weapons.simple_weapons
class Druid(Character):
@@ -205,7 +239,7 @@ class Druid(Character):
'shields (druids will not wear armor or use shields made of metal)',
'clubs', 'daggers', 'darts', 'javelins', 'maces', 'quarterstaffs',
'scimitars', 'sickles', 'slings', 'spears')
weapon_proficienies = (weapons.Club, weapons.Dagger, weapons.Dart,
weapon_proficiencies = (weapons.Club, weapons.Dagger, weapons.Dart,
weapons.Javelin, weapons.Mace, weapons.Quarterstaff,
weapons.Scimitar, weapons.Sickle, weapons.Sling, weapons.Spear)
@@ -215,7 +249,7 @@ class Fighter(Character):
hit_dice_faces = 10
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('All armar', 'shields', 'simple weapons', 'martial weapons')
weapon_proficienies = weapons.simple_weapons + weapons.martial_weapons
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class Monk(Character):
@@ -225,7 +259,7 @@ class Monk(Character):
_proficiencies_text = (
'simple weapons', 'shortswords',
"one type of artisan's tools or one musical instrument")
weapon_proficienies = (weapons.Shortsword,) + weapons.simple_weapons
weapon_proficiencies = (weapons.Shortsword,) + weapons.simple_weapons
class Paladin(Character):
@@ -234,7 +268,7 @@ class Paladin(Character):
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('All armor', 'shields', 'simple weapons',
'martial weapons')
weapon_proficienies = weapons.simple_weapons + weapons.martial_weapons
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class Ranger(Character):
@@ -243,7 +277,7 @@ class Ranger(Character):
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = ("light armor", "medium armor", "shields",
"simple weapons", "martial weapons")
weapon_proficienies = weapons.simple_weapons + weapons.martial_weapons
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class Rogue(Character):
@@ -253,7 +287,7 @@ class Rogue(Character):
_proficiencies_text = (
'light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', "thieves' tools")
weapon_proficienies = (weapons.HandCrossbow, weapons.Longsword,
weapon_proficiencies = (weapons.HandCrossbow, weapons.Longsword,
weapons.Rapier, weapons.Shortsword) + weapons.simple_weapons
@@ -263,7 +297,7 @@ class Sorceror(Character):
saving_throw_proficiencies = ('constitution', 'charisma')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficienies = (weapons.Dagger, weapons.Dart,
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
@@ -273,7 +307,7 @@ class Warlock(Character):
hit_dice_faces = 8
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ("light Armor", "simple weapons")
weapon_proficienies = weapons.simple_weapons
weapon_proficiencies = weapons.simple_weapons
class Wizard(Character):
@@ -282,6 +316,6 @@ class Wizard(Character):
saving_throw_proficiencies = ('intelligence', 'wisdom')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficienies = (weapons.Dagger, weapons.Dart,
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
+4 -2
View File
@@ -61,7 +61,7 @@ def create_fdf(character, fdfname):
('ClassLevel', class_level),
('Background', character.background),
('PlayerName', character.player_name),
('Race ', character.race),
('Race ', str(character.race)),
('Alignment', character.alignment),
('XP', character.xp),
# Abilities
@@ -111,11 +111,13 @@ def create_fdf(character, fdfname):
# Hit points
('HDTotal', character.hit_dice),
('HPMax', character.hp_max),
# Personality traits
# Personality traits and other features
('PersonalityTraits ', text_box(character.personality_traits)),
('Ideals', text_box(character.ideals)),
('Bonds', text_box(character.bonds)),
('Flaws', text_box(character.flaws)),
('AttacksSpellcasting', text_box(character.attacks_and_spellcasting)),
('Features and Traits', text_box(character.features_and_traits)),
# Inventory
('CP', character.cp),
('SP', character.sp),
+124
View File
@@ -0,0 +1,124 @@
from . import weapons
class Race():
name = "Unknown"
size = "medium"
speed = 30
proficiencies_text = tuple()
weapon_proficiences = tuple()
def __str__(self):
return self.name
def __repr__(self):
return f"<self.name>"
# Dwarves
class Dwarf(Race):
name = "Dwarf"
size = "medium"
speed = 25
proficiencies_text = ('battleaxes', 'handaxes', 'throwing hammers', 'warhammers')
weapon_proficiences = (weapons.Battleaxe, weapons.Handaxe,
weapons.ThrowingHammer, weapons.Warhammer)
class HillDwarf(Dwarf):
name = "Hill Dwarf"
class MountainDwarf(Dwarf):
name = "Mountain Dwarf"
# Elves
class Elf(Race):
name = "Elf"
size = "medium"
speed = 30
class HighElf(Elf):
name = "High Elf"
weapon_proficiencies = (weapons.Longsword, weapons.Shortsword,
weapons.Shortbow, weapons.Longbow)
proficiencies_text = ('longswords', 'shortswords', 'shortbows', 'longbows')
class WoodElf(Elf):
name = "Wood Elf"
weapon_proficiencies = (weapons.Longsword, weapons.Shortsword,
weapons.Shortbow, weapons.Longbow)
proficiencies_text = ('longswords', 'shortswords', 'shortbows', 'longbows')
class DarkElf(Elf):
name = "Dark Elf"
weapon_proficiencies = (weapons.Rapier, weapons.Shortsword, weapons.HandCrossbow)
proficiencies_text = ('repiers', 'shortswords', 'hand crossbows')
# Halflings
class Halfling(Race):
name = "Halfling"
size = "small"
speed = 25
class LightfootHalfling(Halfling):
name = "Lightfoot Halfling"
class StoutHalfling(Halfling):
name = "Stout Halfling"
# Humans
class Human(Race):
name = "Human"
size = "medium"
speed = 30
# Dragonborn
class Dragonborn(Race):
name = "Dragonborn"
size = "medium"
speed = 30
# Gnomes
class Gnome(Race):
name = "Gnome"
size = "small"
speed = 25
class ForestGnome(Gnome):
name = "Forest Gnome"
class RockGnome(Gnome):
name = "Rock Gnome"
# Half-elves
class HalfElf(Race):
name = "Half-Elf"
size = "medium"
speed = 30
# Half-Orcs
class HalfOrc(Race):
name = "Half-Orc"
size = "medium"
speed = 30
# Tielflings
class Tiefling(Race):
name = "Tiefling"
size = "medium"
speed = 30
+14 -2
View File
@@ -305,6 +305,16 @@ class Shortsword(Weapon):
ability = 'strength'
class ThrowingHammer(Weapon):
name = "Throwing Hammer"
cost = "15 gp"
base_damage = '1d6'
damage_type = "bludgeoning"
weight = 4
properties = "Thrown (range 60/120)"
ability = "strength"
class Trident(Weapon):
name = "Trident"
cost = "5 gp"
@@ -403,8 +413,10 @@ simple_ranged_weapons = (LightCrossbow, Dart, Shortbow, Sling)
simple_weapons = simple_melee_weapons + simple_ranged_weapons
martial_melee_weapons = (Battleaxe, Flail, Glaive, Greataxe,
Greatsword, Halberd, Lance, Longsword, Maul, Morningstar, Pike,
Rapier, Scimitar, Shortsword, Trident, WarPick, Warhammer, Whip)
Greatsword, Halberd, Lance, Longsword, Maul,
Morningstar, Pike, Rapier, Scimitar,
Shortsword, ThrowingHammer, Trident, WarPick,
Warhammer, Whip)
martial_ranged_weapons = (Blowgun, HandCrossbow, HeavyCrossbow,
Longbow, Net)
martial_weapons = martial_melee_weapons + martial_ranged_weapons
Binary file not shown.
+39 -1
View File
@@ -7,7 +7,6 @@ level = 3
alignment = "Neutral"
xp = 1984
hp_max = 19
speed = 25
# Ability Scores
strength = 8
@@ -41,6 +40,45 @@ equipment = (
tinderbox, waterskin, crowbar, set of dark common clothes
including a hood, pouch.""")
attacks_and_spellcasting = (
"""Sneak Attack: Once per turn, when you hit a creature with a
Dexterity-based attack (such as with your shortsword or shortbow)
and you have advantage on the attack roll, you can deal an extra
1d6 damage to your target. You dont need advantage if another
enemy of the target is within 5 feet of it and isnt
incapacitated. You cant deal the extra damage, however, if you
have disadvantage on the attack roll.""")
features_and_traits = (
"""Thieves' Cant: You know thieves cant, a secret mix of dialect,
jargon, and code that allows you to hide messages in seemingly
normal conversation. You also understand a set of secret signs and
symbols used to convey short, simple messages, such as whether an
area is dangerous, whether loot is nearby, or whether the people
in an area are easy marks or will provide a safe house for thieves
on the run.
Lucky: When you roll a natural 1 on an attack roll, ability check,
or saving throw, you can reroll the die and must use the new roll.
Brave: You have advantage on saving throws against being
frightened.
Halfling Nimbleness: You can move through the space of any
creature that is of a size larger than yours.
Naturally Stealthy: You can attempt to hide when you are obscured
by a creature that is at least one size larger than you.
Criminal Contact: You have a contact who acts as your liaison to a
network of other criminals. You know how to get messages to and
from your contact, even over great distances; you know the local
messengers, corrupt caravan masters, and seedy sailors who can
carry messages for you. You can move secret information or stolen
goods through your contact in exchange for money or other
information you seek.""")
# Backstory
personality_traits = """I never have a plan, but Im great at making things up as I go
along. Also, the best way to get me to do something is to tell me I
+38 -3
View File
@@ -2,7 +2,8 @@
from unittest import TestCase
from dungeonsheets.character import Character
from dungeonsheets import race
from dungeonsheets.character import Character, Wizard
from dungeonsheets.weapons import Weapon, Shortsword
@@ -27,11 +28,14 @@ class TestCharacter(TestCase):
char.set_attrs(weapons=['shortsword'])
self.assertEqual(len(char.weapons), 1)
self.assertTrue(isinstance(char.weapons[0], Shortsword))
# Check that race gets set to an object
char.set_attrs(race='high elf')
self.assertIsInstance(char.race, race.HighElf)
def test_wield_weapon(self):
char = Character()
char.strength = 14
char.weapon_proficienies = [Shortsword]
char.weapon_proficiencies = [Shortsword]
# Add a weapon
char.wield_weapon('shortsword')
self.assertEqual(len(char.weapons), 1)
@@ -46,7 +50,33 @@ class TestCharacter(TestCase):
char.wield_weapon('shortsword')
sword = char.weapons[0]
self.assertEqual(sword.attack_bonus, 5) # dex + prof
# Check if race weapon proficiencies are considered
char.weapons = []
char.weapon_proficiencies = []
char.race = race.HighElf()
char.wield_weapon('shortsword')
sword = char.weapons[0]
self.assertEqual(sword.attack_bonus, 5)
def test_str(self):
char = Wizard(name="Inara")
self.assertEqual(str(char), 'Inara')
self.assertEqual(repr(char), '<Wizard: Inara>')
def test_is_proficient(self):
char = Character()
char.weapon_proficiencies
sword = Shortsword()
# Check for not-proficient weapon
self.assertFalse(char.is_proficient(sword))
# Check if we're proficient in the weapon
char.weapon_proficiencies = [Shortsword]
self.assertTrue(char.is_proficient(sword))
# Now try it with a racial proficiency
char.weapon_proficiencies = tuple()
char.race = race.HighElf()
self.assertTrue(char.is_proficient(sword))
def test_proficiencies_text(self):
char = Character()
char._proficiencies_text = ('hello', 'world')
@@ -54,6 +84,11 @@ class TestCharacter(TestCase):
# Check for extra proficiencies
char.proficiencies_extra = ("it's", "me")
self.assertEqual(char.proficiencies_text, "Hello, world, it's, me.")
# Check that race proficienceis are included
elf = race.HighElf()
char.race = elf
expected = "Hello, world, longswords, shortswords, shortbows, longbows, it's, me."
self.assertEqual(char.proficiencies_text, expected)
def test_proficiency_bonus(self):
char = Character()