mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-07 05:03:31 +02:00
Remove encounters, refactor Actor into Entity, move more core capabilities into Entity
This commit is contained in:
@@ -23,14 +23,7 @@ from dungeonsheets import (
|
|||||||
from dungeonsheets.stats import findattr
|
from dungeonsheets.stats import findattr
|
||||||
from dungeonsheets.weapons import Weapon
|
from dungeonsheets.weapons import Weapon
|
||||||
from dungeonsheets.readers import read_character_file
|
from dungeonsheets.readers import read_character_file
|
||||||
from dungeonsheets.encounter.agent import Agent
|
from dungeonsheets.entity import Entity
|
||||||
|
|
||||||
|
|
||||||
def read(fname):
|
|
||||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = read("../VERSION").strip()
|
|
||||||
|
|
||||||
|
|
||||||
dice_re = re.compile(r"(\d+)d(\d+)")
|
dice_re = re.compile(r"(\d+)d(\d+)")
|
||||||
@@ -146,17 +139,15 @@ def _resolve_mechanic(mechanic, module, SuperClass, warning_message=None):
|
|||||||
return Mechanic
|
return Mechanic
|
||||||
|
|
||||||
|
|
||||||
class Character(Agent):
|
class Character(Entity):
|
||||||
"""A generic player character."""
|
"""A generic player character."""
|
||||||
|
|
||||||
|
# Character-specific
|
||||||
player_name = ""
|
player_name = ""
|
||||||
|
|
||||||
dungeonsheets_version = __version__
|
|
||||||
xp = 0
|
xp = 0
|
||||||
inspiration = False
|
inspiration = False
|
||||||
attacks_and_spellcasting = ""
|
attacks_and_spellcasting = ""
|
||||||
class_list = list()
|
class_list = list()
|
||||||
_race = None
|
|
||||||
_background = None
|
_background = None
|
||||||
|
|
||||||
# Characteristics
|
# Characteristics
|
||||||
@@ -172,7 +163,7 @@ class Character(Agent):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
classes: Sequence = [],
|
class_list: Sequence = [],
|
||||||
levels: Sequence[int] = [],
|
levels: Sequence[int] = [],
|
||||||
subclasses: Sequence = [],
|
subclasses: Sequence = [],
|
||||||
**attrs,
|
**attrs,
|
||||||
@@ -184,7 +175,7 @@ class Character(Agent):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
==========
|
==========
|
||||||
classes
|
class_list
|
||||||
Strings with class names, or character class definitions
|
Strings with class names, or character class definitions
|
||||||
representing the characters various D&D classes.
|
representing the characters various D&D classes.
|
||||||
levels
|
levels
|
||||||
@@ -200,7 +191,7 @@ class Character(Agent):
|
|||||||
super(Character, self).__init__()
|
super(Character, self).__init__()
|
||||||
self.clear()
|
self.clear()
|
||||||
# make sure class, race, background are set first
|
# make sure class, race, background are set first
|
||||||
my_classes = classes
|
my_classes = class_list
|
||||||
my_levels = levels
|
my_levels = levels
|
||||||
my_subclasses = subclasses
|
my_subclasses = subclasses
|
||||||
# backwards compatability
|
# backwards compatability
|
||||||
@@ -931,7 +922,7 @@ class Character(Agent):
|
|||||||
make_sheet(filename, character=self, flatten=kwargs.get("flatten", True))
|
make_sheet(filename, character=self, flatten=kwargs.get("flatten", True))
|
||||||
|
|
||||||
|
|
||||||
# Add backwards compatability for tests
|
# Add backwards compatibility for tests
|
||||||
class Artificer(Character):
|
class Artificer(Character):
|
||||||
def __init__(self, level=1, **attrs):
|
def __init__(self, level=1, **attrs):
|
||||||
attrs["classes"] = ["Artificer"]
|
attrs["classes"] = ["Artificer"]
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
class Condition:
|
|
||||||
"""A condition that can be held by an agent"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Blinded(Condition):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Charmed(Condition):
|
|
||||||
pass
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from dungeonsheets.encounter.encounter import Encounter
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
from dungeonsheets.encounter.events import Event
|
|
||||||
|
|
||||||
|
|
||||||
class Executable(ABC):
|
|
||||||
"""Something (like an action) that can be executed.
|
|
||||||
|
|
||||||
Executing an action results in an event that is stored
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def execute(self, subj, obj=None):
|
|
||||||
"""Execute the given action"""
|
|
||||||
|
|
||||||
|
|
||||||
class Action(Executable):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BonusAction(Executable):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Reaction(Executable):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Movement(Executable):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LairAction(Executable):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LegendaryAction(Executable):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Attack(Action):
|
|
||||||
|
|
||||||
def __init__(self, subj, obj):
|
|
||||||
self.subj = subj
|
|
||||||
self.obj = obj
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
from dungeonsheets.conditions.conditions import Blinded, Charmed
|
|
||||||
from dungeonsheets.encounter.actions import Attack
|
|
||||||
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill, \
|
|
||||||
NumericalInitiative
|
|
||||||
from abc import ABC
|
|
||||||
from dungeonsheets.dice import roll
|
|
||||||
|
|
||||||
|
|
||||||
class Agent(ABC):
|
|
||||||
"""An actor in an encounter. Use Monster or Character, not this class directly!"""
|
|
||||||
|
|
||||||
# General attributes
|
|
||||||
name = ""
|
|
||||||
alignment = "Neutral"
|
|
||||||
|
|
||||||
# Hit points
|
|
||||||
hp_max = None
|
|
||||||
|
|
||||||
# Base stats (ability scores)
|
|
||||||
strength = Ability()
|
|
||||||
dexterity = Ability()
|
|
||||||
constitution = Ability()
|
|
||||||
intelligence = Ability()
|
|
||||||
wisdom = Ability()
|
|
||||||
charisma = Ability()
|
|
||||||
|
|
||||||
# Numerical things
|
|
||||||
armor_class = ArmorClass()
|
|
||||||
initiative = Initiative()
|
|
||||||
speed = Speed()
|
|
||||||
|
|
||||||
# Proficiencies and Languages
|
|
||||||
_saving_throw_proficiencies = tuple() # use to overwrite class proficiencies
|
|
||||||
other_weapon_proficiencies = tuple() # add to class/race proficiencies
|
|
||||||
skill_proficiencies = list()
|
|
||||||
skill_expertise = list()
|
|
||||||
languages = ""
|
|
||||||
|
|
||||||
# Skills
|
|
||||||
acrobatics = Skill(ability="dexterity")
|
|
||||||
animal_handling = Skill(ability="wisdom")
|
|
||||||
arcana = Skill(ability="intelligence")
|
|
||||||
athletics = Skill(ability="strength")
|
|
||||||
deception = Skill(ability="charisma")
|
|
||||||
history = Skill(ability="intelligence")
|
|
||||||
insight = Skill(ability="wisdom")
|
|
||||||
intimidation = Skill(ability="charisma")
|
|
||||||
investigation = Skill(ability="intelligence")
|
|
||||||
medicine = Skill(ability="wisdom")
|
|
||||||
nature = Skill(ability="intelligence")
|
|
||||||
perception = Skill(ability="wisdom")
|
|
||||||
performance = Skill(ability="charisma")
|
|
||||||
persuasion = Skill(ability="charisma")
|
|
||||||
religion = Skill(ability="intelligence")
|
|
||||||
sleight_of_hand = Skill(ability="dexterity")
|
|
||||||
stealth = Skill(ability="dexterity")
|
|
||||||
survival = Skill(ability="wisdom")
|
|
||||||
|
|
||||||
# Conditions
|
|
||||||
blinded = Blinded()
|
|
||||||
charmed = Charmed()
|
|
||||||
# TODO finish me!
|
|
||||||
|
|
||||||
# Inventory
|
|
||||||
cp = 0
|
|
||||||
sp = 0
|
|
||||||
ep = 0
|
|
||||||
gp = 0
|
|
||||||
pp = 0
|
|
||||||
equipment = ""
|
|
||||||
weapons = list()
|
|
||||||
magic_items = list()
|
|
||||||
armor = None
|
|
||||||
shield = None
|
|
||||||
|
|
||||||
# Magic
|
|
||||||
spellcasting_ability = None
|
|
||||||
_spells = list()
|
|
||||||
_spells_prepared = list()
|
|
||||||
infusions = list()
|
|
||||||
|
|
||||||
# Features IN MAJOR DEVELOPMENT
|
|
||||||
custom_features = list()
|
|
||||||
feature_choices = list()
|
|
||||||
|
|
||||||
# Current Status:
|
|
||||||
numerical_initiative = NumericalInitiative()
|
|
||||||
_initiative_roll = False
|
|
||||||
_current_hp = None
|
|
||||||
statuses = list()
|
|
||||||
|
|
||||||
# TODO: Pull in the monster class-variables here too
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.default_actions = list()
|
|
||||||
self.default_bonus_actions = list()
|
|
||||||
self.default_reactions = list()
|
|
||||||
self.default_legendary_actions = list()
|
|
||||||
self.default_lair_actions = list()
|
|
||||||
|
|
||||||
def roll_initiative(self):
|
|
||||||
init_mod, adv = self.numerical_initiative
|
|
||||||
val = roll(20)
|
|
||||||
if adv:
|
|
||||||
val = max(val, roll(20))
|
|
||||||
self._initiative_roll = val + init_mod
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_hp(self):
|
|
||||||
if self._current_hp is None:
|
|
||||||
self._current_hp = self.hp_max
|
|
||||||
return self._current_hp
|
|
||||||
|
|
||||||
@current_hp.setter
|
|
||||||
def current_hp(self, val):
|
|
||||||
if val < 0:
|
|
||||||
self._current_hp = 0
|
|
||||||
else:
|
|
||||||
self._current_hp = val
|
|
||||||
|
|
||||||
@property
|
|
||||||
def initiative_roll(self):
|
|
||||||
if self._initiative_roll is False:
|
|
||||||
self.roll_initiative()
|
|
||||||
return self._initiative_roll
|
|
||||||
|
|
||||||
def make_actions(self, encounter):
|
|
||||||
"""Return a series of actions"""
|
|
||||||
|
|
||||||
# TODO: Dramatically improve logic, consider healing,
|
|
||||||
# consider encounter state, consider strategy etc.
|
|
||||||
best_opponent = encounter.opponents(self)[0] # TODO: Choose opponent cleverly
|
|
||||||
action = self.actions[0](self, best_opponent)
|
|
||||||
event = action.execute()
|
|
||||||
encounter.events.append(event)
|
|
||||||
return [event] # TODO: Also allow bonus actions, etc.
|
|
||||||
|
|
||||||
def long_rest(self):
|
|
||||||
self.current_hp = self.hp_max
|
|
||||||
# TODO: Support spell slots
|
|
||||||
self.new_turn()
|
|
||||||
|
|
||||||
def new_turn(self):
|
|
||||||
self._actions = self.default_actions
|
|
||||||
self._bonus_actions = self.default_bonus_actions
|
|
||||||
self._reactions = self.default_reactions
|
|
||||||
self._legendary_actions = self.default_legendary_actions
|
|
||||||
self._lair_actions = self.default_lair_actions
|
|
||||||
|
|
||||||
def has_feature(self, *args, **kwargs):
|
|
||||||
return False # TODO: Save list of monster features as a list to check
|
|
||||||
|
|
||||||
# TODO: Consider having a single list of actions and gain or lose them each
|
|
||||||
# turn based on interrogating their sub-type instead, using isinstance or
|
|
||||||
# another method.
|
|
||||||
|
|
||||||
@property
|
|
||||||
def actions(self):
|
|
||||||
"""All the remaining things I can do in a turn"""
|
|
||||||
return self._actions
|
|
||||||
|
|
||||||
@property
|
|
||||||
def movement(self):
|
|
||||||
"""The rest of where I can go in a turn"""
|
|
||||||
return self._movement
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bonus_actions(self):
|
|
||||||
"""The rest of the things I can do once in addition to an action"""
|
|
||||||
return self._bonus_actions
|
|
||||||
|
|
||||||
@property
|
|
||||||
def reactions(self):
|
|
||||||
"""The remaining things I can do in response to an action"""
|
|
||||||
return self._reactions
|
|
||||||
|
|
||||||
@property
|
|
||||||
def lair_actions(self):
|
|
||||||
"""Remaining things I can do at initiative count 20"""
|
|
||||||
return self._lair_actions
|
|
||||||
|
|
||||||
@property
|
|
||||||
def legendary_actions(self):
|
|
||||||
"""Remaining things I can do only so many times in a turn after another agent acts"""
|
|
||||||
return self._legendary_actions
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
class Encounter:
|
|
||||||
"""A combat encounter between two parties -- good guys and bad guys"""
|
|
||||||
|
|
||||||
def __init__(self, group_a, group_b):
|
|
||||||
self.group_a = group_a
|
|
||||||
self.group_b = group_b
|
|
||||||
self.all_agents = group_a + group_b
|
|
||||||
|
|
||||||
self.events = [] # Should be private?
|
|
||||||
|
|
||||||
def opponents(self, agent):
|
|
||||||
"""Who opposes the given agent in an encounter?"""
|
|
||||||
if agent in self.group_a:
|
|
||||||
return self.group_b
|
|
||||||
else:
|
|
||||||
return self.group_a
|
|
||||||
|
|
||||||
def allies(self, agent):
|
|
||||||
"""Who sides with the given agent in an encounter?"""
|
|
||||||
if agent in self.group_a:
|
|
||||||
return list(set(self.group_a) - set([agent]))
|
|
||||||
else:
|
|
||||||
return list(set(self.group_b) - set([agent]))
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.events = []
|
|
||||||
self.long_rest()
|
|
||||||
|
|
||||||
def simulate(self):
|
|
||||||
"""Who will win?"""
|
|
||||||
|
|
||||||
# Initiative
|
|
||||||
for agent in self.all_agents:
|
|
||||||
agent.roll_initiative()
|
|
||||||
|
|
||||||
self.all_agents = sorted(self.all_agents, key=lambda a: a.initiative_roll)
|
|
||||||
|
|
||||||
# TODO: Support Lair Actions, cleverer loop
|
|
||||||
while not self.is_encounter_over():
|
|
||||||
self.new_turn()
|
|
||||||
for agent in self.all_agents:
|
|
||||||
agent.make_actions(self)
|
|
||||||
if self.is_encounter_over():
|
|
||||||
return self.events
|
|
||||||
|
|
||||||
return self.events # Should never get here -- self.is_encounter_over() will end it
|
|
||||||
|
|
||||||
def rating(self):
|
|
||||||
"""Encounter Rating"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def is_encounter_over(self):
|
|
||||||
"""If all members of one party are at HP <= 0, it's over"""
|
|
||||||
return (
|
|
||||||
all([agent.current_hp <= 0 for agent in self.group_a]) or
|
|
||||||
all([agent.current_hp <= 0 for agent in self.group_b])
|
|
||||||
)
|
|
||||||
|
|
||||||
def long_rest(self):
|
|
||||||
"""Resets all agents to have full actions, abilities, etc."""
|
|
||||||
for agent in self.all_agents:
|
|
||||||
agent.long_rest()
|
|
||||||
|
|
||||||
def new_turn(self):
|
|
||||||
"""Resets turn-based actions for all agents"""
|
|
||||||
for agent in self.all_agents:
|
|
||||||
agent.new_turn()
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
class Event:
|
|
||||||
"""An event between one and possibly more entities"""
|
|
||||||
|
|
||||||
def __init__(self, action, *args, **kwargs):
|
|
||||||
self.action = action
|
|
||||||
self.subj_hp = action.subj.current_hp
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AttackEvent(Event):
|
|
||||||
"""An attack action completed"""
|
|
||||||
|
|
||||||
def __init__(self, action, result, damage, is_hit):
|
|
||||||
super(AttackEvent, self).__init__(action)
|
|
||||||
if hasattr(self.action, "obj"):
|
|
||||||
self.obj_hp = self.action.obj.current_hp
|
|
||||||
self.result = result
|
|
||||||
self.damage = damage
|
|
||||||
self.is_hit = is_hit
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
|
|
||||||
if self.is_hit:
|
|
||||||
return f"{self.action.subj.name} Hit! with a {self.result} for {self.damage} damage, leaving {self.action.obj.name} with {self.obj_hp} hitpoints"
|
|
||||||
else:
|
|
||||||
return f"{self.action.subj.name} Missed! with a {self.result}. {self.action.obj.name} has {self.obj_hp} hp remaining."
|
|
||||||
|
|
||||||
# TODO: Support more events
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill,
|
||||||
|
from abc import ABC
|
||||||
|
import os
|
||||||
|
|
||||||
|
def read(fname):
|
||||||
|
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = read("../VERSION").strip()
|
||||||
|
|
||||||
|
class Entity(ABC):
|
||||||
|
"""A thing with stats. Use Monster or Character, not this class directly!"""
|
||||||
|
|
||||||
|
# General attributes
|
||||||
|
dungeonsheets_version = __version__
|
||||||
|
name = ""
|
||||||
|
alignment = "Neutral"
|
||||||
|
_race = None
|
||||||
|
|
||||||
|
# Hit points
|
||||||
|
hp_max = None
|
||||||
|
|
||||||
|
# Base stats (ability scores)
|
||||||
|
strength = Ability()
|
||||||
|
dexterity = Ability()
|
||||||
|
constitution = Ability()
|
||||||
|
intelligence = Ability()
|
||||||
|
wisdom = Ability()
|
||||||
|
charisma = Ability()
|
||||||
|
|
||||||
|
# Numerical things
|
||||||
|
armor_class = ArmorClass()
|
||||||
|
initiative = Initiative()
|
||||||
|
speed = Speed()
|
||||||
|
|
||||||
|
# Proficiencies and Languages
|
||||||
|
_saving_throw_proficiencies = tuple() # use to overwrite class proficiencies
|
||||||
|
other_weapon_proficiencies = tuple() # add to class/race proficiencies
|
||||||
|
skill_proficiencies = list()
|
||||||
|
skill_expertise = list()
|
||||||
|
languages = ""
|
||||||
|
senses = ""
|
||||||
|
|
||||||
|
# Skills
|
||||||
|
acrobatics = Skill(ability="dexterity")
|
||||||
|
animal_handling = Skill(ability="wisdom")
|
||||||
|
arcana = Skill(ability="intelligence")
|
||||||
|
athletics = Skill(ability="strength")
|
||||||
|
deception = Skill(ability="charisma")
|
||||||
|
history = Skill(ability="intelligence")
|
||||||
|
insight = Skill(ability="wisdom")
|
||||||
|
intimidation = Skill(ability="charisma")
|
||||||
|
investigation = Skill(ability="intelligence")
|
||||||
|
medicine = Skill(ability="wisdom")
|
||||||
|
nature = Skill(ability="intelligence")
|
||||||
|
perception = Skill(ability="wisdom")
|
||||||
|
performance = Skill(ability="charisma")
|
||||||
|
persuasion = Skill(ability="charisma")
|
||||||
|
religion = Skill(ability="intelligence")
|
||||||
|
sleight_of_hand = Skill(ability="dexterity")
|
||||||
|
stealth = Skill(ability="dexterity")
|
||||||
|
survival = Skill(ability="wisdom")
|
||||||
|
|
||||||
|
# Inventory
|
||||||
|
cp = 0
|
||||||
|
sp = 0
|
||||||
|
ep = 0
|
||||||
|
gp = 0
|
||||||
|
pp = 0
|
||||||
|
equipment = ""
|
||||||
|
weapons = list()
|
||||||
|
magic_items = list()
|
||||||
|
armor = None
|
||||||
|
shield = None
|
||||||
|
|
||||||
|
# Magic
|
||||||
|
spellcasting_ability = None
|
||||||
|
_spells = list()
|
||||||
|
_spells_prepared = list()
|
||||||
|
infusions = list()
|
||||||
|
|
||||||
|
# Features IN MAJOR DEVELOPMENT
|
||||||
|
custom_features = list()
|
||||||
|
feature_choices = list()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
@@ -3,27 +3,17 @@ shape forms."""
|
|||||||
|
|
||||||
|
|
||||||
from dungeonsheets.stats import Ability
|
from dungeonsheets.stats import Ability
|
||||||
from dungeonsheets.encounter.agent import Agent
|
from dungeonsheets.entity import Entity
|
||||||
|
|
||||||
|
|
||||||
class Monster(Agent):
|
class Monster(Entity):
|
||||||
"""A monster that may be encountered when adventuring."""
|
"""A monster that may be encountered when adventuring."""
|
||||||
|
|
||||||
name = "Generic Monster"
|
name = "Generic Monster"
|
||||||
description = ""
|
description = ""
|
||||||
challenge_rating = 0
|
challenge_rating = 0
|
||||||
armor_class = 0
|
|
||||||
skills = "Perception +3, Stealth +4"
|
skills = "Perception +3, Stealth +4"
|
||||||
senses = ""
|
swim_speed = 0 # TODO: Consider refactoring stats.Speed to consider all of these just like we do stats.Ability
|
||||||
languages = ""
|
|
||||||
strength = Ability()
|
|
||||||
dexterity = Ability()
|
|
||||||
constitution = Ability()
|
|
||||||
intelligence = Ability()
|
|
||||||
wisdom = Ability()
|
|
||||||
charisma = Ability()
|
|
||||||
speed = 30
|
|
||||||
swim_speed = 0
|
|
||||||
fly_speed = 0
|
fly_speed = 0
|
||||||
hp_max = 10
|
hp_max = 10
|
||||||
hit_dice = "1d6"
|
hit_dice = "1d6"
|
||||||
|
|||||||
+58
-62
@@ -64,10 +64,6 @@ def findattr(obj, name):
|
|||||||
def mod_str(modifier):
|
def mod_str(modifier):
|
||||||
"""Converts a modifier to a string, eg 2 -> '+2'."""
|
"""Converts a modifier to a string, eg 2 -> '+2'."""
|
||||||
return "{:+d}".format(modifier)
|
return "{:+d}".format(modifier)
|
||||||
if modifier == 0:
|
|
||||||
return str(modifier)
|
|
||||||
else:
|
|
||||||
return "{:+}".format(modifier)
|
|
||||||
|
|
||||||
|
|
||||||
AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw"))
|
AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw"))
|
||||||
@@ -90,25 +86,25 @@ class Ability:
|
|||||||
# ability score dictionary exists but doesn't have this ability
|
# ability score dictionary exists but doesn't have this ability
|
||||||
obj._ability_scores[self.ability_name] = self.default_value
|
obj._ability_scores[self.ability_name] = self.default_value
|
||||||
|
|
||||||
def __get__(self, character, Character):
|
def __get__(self, entity, Entity):
|
||||||
self._check_dict(character)
|
self._check_dict(entity)
|
||||||
score = character._ability_scores[self.ability_name]
|
score = entity._ability_scores[self.ability_name]
|
||||||
modifier = math.floor((score - 10) / 2)
|
modifier = math.floor((score - 10) / 2)
|
||||||
# Check for proficiency
|
# Check for proficiency
|
||||||
saving_throw = modifier
|
saving_throw = modifier
|
||||||
if self.ability_name is not None and hasattr(
|
if self.ability_name is not None and hasattr(
|
||||||
character, "saving_throw_proficiencies"
|
entity, "saving_throw_proficiencies"
|
||||||
):
|
):
|
||||||
is_proficient = self.ability_name in character.saving_throw_proficiencies
|
is_proficient = self.ability_name in entity.saving_throw_proficiencies
|
||||||
if is_proficient:
|
if is_proficient:
|
||||||
saving_throw += character.proficiency_bonus
|
saving_throw += entity.proficiency_bonus
|
||||||
# Create the named tuple
|
# Create the named tuple
|
||||||
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw)
|
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def __set__(self, character, val):
|
def __set__(self, entity, val):
|
||||||
self._check_dict(character)
|
self._check_dict(entity)
|
||||||
character._ability_scores[self.ability_name] = val
|
entity._ability_scores[self.ability_name] = val
|
||||||
self.value = val
|
self.value = val
|
||||||
|
|
||||||
|
|
||||||
@@ -118,27 +114,27 @@ class Skill:
|
|||||||
def __init__(self, ability):
|
def __init__(self, ability):
|
||||||
self.ability_name = ability
|
self.ability_name = ability
|
||||||
|
|
||||||
def __set_name__(self, character, name):
|
def __set_name__(self, entity, name):
|
||||||
self.skill_name = name.lower().replace("_", " ")
|
self.skill_name = name.lower().replace("_", " ")
|
||||||
self.character = character
|
self.character = entity
|
||||||
|
|
||||||
def __get__(self, character, owner):
|
def __get__(self, entity, owner):
|
||||||
ability = getattr(character, self.ability_name)
|
ability = getattr(entity, self.ability_name)
|
||||||
modifier = ability.modifier
|
modifier = ability.modifier
|
||||||
# Check for proficiency
|
# Check for proficiency
|
||||||
is_proficient = self.skill_name in character.skill_proficiencies
|
is_proficient = self.skill_name in entity.skill_proficiencies
|
||||||
if is_proficient:
|
if is_proficient:
|
||||||
modifier += character.proficiency_bonus
|
modifier += entity.proficiency_bonus
|
||||||
elif character.has_feature(JackOfAllTrades):
|
elif entity.has_feature(JackOfAllTrades):
|
||||||
modifier += character.proficiency_bonus // 2
|
modifier += entity.proficiency_bonus // 2
|
||||||
elif character.has_feature(RemarkableAthelete):
|
elif entity.has_feature(RemarkableAthelete):
|
||||||
if self.ability_name.lower() in ("strength", "dexterity", "constitution"):
|
if self.ability_name.lower() in ("strength", "dexterity", "constitution"):
|
||||||
modifier += ceil(character.proficienc_bonus / 2.0)
|
modifier += ceil(entity.proficienc_bonus / 2.0)
|
||||||
|
|
||||||
# Check for expertise
|
# Check for expertise
|
||||||
is_expert = self.skill_name in character.skill_expertise
|
is_expert = self.skill_name in entity.skill_expertise
|
||||||
if is_expert:
|
if is_expert:
|
||||||
modifier += character.proficiency_bonus
|
modifier += entity.proficiency_bonus
|
||||||
return modifier
|
return modifier
|
||||||
|
|
||||||
|
|
||||||
@@ -147,36 +143,36 @@ class ArmorClass:
|
|||||||
The Armor Class of a character
|
The Armor Class of a character
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __get__(self, char, Character):
|
def __get__(self, entity, Entity):
|
||||||
armor = char.armor or NoArmor()
|
armor = entity.armor or NoArmor()
|
||||||
ac = armor.base_armor_class
|
ac = armor.base_armor_class
|
||||||
# calculate and apply modifiers
|
# calculate and apply modifiers
|
||||||
if armor.dexterity_mod_max is None:
|
if armor.dexterity_mod_max is None:
|
||||||
ac += char.dexterity.modifier
|
ac += entity.dexterity.modifier
|
||||||
else:
|
else:
|
||||||
ac += min(char.dexterity.modifier, armor.dexterity_mod_max)
|
ac += min(entity.dexterity.modifier, armor.dexterity_mod_max)
|
||||||
if char.has_feature(NaturalArmor):
|
if entity.has_feature(NaturalArmor):
|
||||||
ac = max(ac, 13 + char.dexterity.modifier)
|
ac = max(ac, 13 + entity.dexterity.modifier)
|
||||||
shield = char.shield or NoShield()
|
shield = entity.shield or NoShield()
|
||||||
ac += shield.base_armor_class
|
ac += shield.base_armor_class
|
||||||
# Compute feature-specific additions
|
# Compute feature-specific additions
|
||||||
if char.has_feature(UnarmoredDefenseMonk):
|
if entity.has_feature(UnarmoredDefenseMonk):
|
||||||
if isinstance(armor, NoArmor) and isinstance(shield, NoShield):
|
if isinstance(armor, NoArmor) and isinstance(shield, NoShield):
|
||||||
ac += char.wisdom.modifier
|
ac += entity.wisdom.modifier
|
||||||
if char.has_feature(UnarmoredDefenseBarbarian):
|
if entity.has_feature(UnarmoredDefenseBarbarian):
|
||||||
if isinstance(armor, NoArmor):
|
if isinstance(armor, NoArmor):
|
||||||
ac += char.constitution.modifier
|
ac += entity.constitution.modifier
|
||||||
if char.has_feature(DraconicResilience):
|
if entity.has_feature(DraconicResilience):
|
||||||
if isinstance(armor, NoArmor):
|
if isinstance(armor, NoArmor):
|
||||||
ac += 3
|
ac += 3
|
||||||
if char.has_feature(Defense):
|
if entity.has_feature(Defense):
|
||||||
if not isinstance(armor, NoArmor):
|
if not isinstance(armor, NoArmor):
|
||||||
ac += 1
|
ac += 1
|
||||||
if char.has_feature(SoulOfTheForge):
|
if entity.has_feature(SoulOfTheForge):
|
||||||
if isinstance(armor, HeavyArmor):
|
if isinstance(armor, HeavyArmor):
|
||||||
ac += 1
|
ac += 1
|
||||||
# Check if any magic items add to AC
|
# Check if any magic items add to AC
|
||||||
for mitem in char.magic_items:
|
for mitem in entity.magic_items:
|
||||||
if hasattr(mitem, "ac_bonus"):
|
if hasattr(mitem, "ac_bonus"):
|
||||||
ac += mitem.ac_bonus
|
ac += mitem.ac_bonus
|
||||||
return ac
|
return ac
|
||||||
@@ -187,25 +183,25 @@ class Speed:
|
|||||||
The speed of a character
|
The speed of a character
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __get__(self, char, Character):
|
def __get__(self, entity, Entity):
|
||||||
speed = char.race.speed
|
speed = entity.race.speed
|
||||||
other_speed = ""
|
other_speed = ""
|
||||||
if isinstance(speed, str):
|
if isinstance(speed, str):
|
||||||
other_speed = speed[2:]
|
other_speed = speed[2:]
|
||||||
speed = int(speed[:2]) # ignore other speeds, like fly
|
speed = int(speed[:2]) # ignore other speeds, like fly
|
||||||
if char.has_feature(FastMovement):
|
if entity.has_feature(FastMovement):
|
||||||
if not isinstance(char.armor, HeavyArmor):
|
if not isinstance(entity.armor, HeavyArmor):
|
||||||
speed += 10
|
speed += 10
|
||||||
if char.has_feature(SuperiorMobility):
|
if entity.has_feature(SuperiorMobility):
|
||||||
speed += 10
|
speed += 10
|
||||||
if isinstance(char.armor, NoArmor) or (char.armor is None):
|
if isinstance(entity.armor, NoArmor) or (entity.armor is None):
|
||||||
for f in char.features:
|
for f in entity.features:
|
||||||
if isinstance(f, UnarmoredMovement):
|
if isinstance(f, UnarmoredMovement):
|
||||||
speed += f.speed_bonus
|
speed += f.speed_bonus
|
||||||
if char.has_feature(GiftOfTheDepths):
|
if entity.has_feature(GiftOfTheDepths):
|
||||||
if "swim" not in other_speed:
|
if "swim" not in other_speed:
|
||||||
other_speed += " ({:d} swim)".format(speed)
|
other_speed += " ({:d} swim)".format(speed)
|
||||||
if char.has_feature(SeaSoul):
|
if entity.has_feature(SeaSoul):
|
||||||
if "swim" not in other_speed:
|
if "swim" not in other_speed:
|
||||||
other_speed += " (30 swim)"
|
other_speed += " (30 swim)"
|
||||||
return "{:d}{:s}".format(speed, other_speed)
|
return "{:d}{:s}".format(speed, other_speed)
|
||||||
@@ -214,19 +210,19 @@ class Speed:
|
|||||||
class NumericalInitiative:
|
class NumericalInitiative:
|
||||||
"""A numerical representation of initiative"""
|
"""A numerical representation of initiative"""
|
||||||
|
|
||||||
def __get__(self, char, Character):
|
def __get__(self, entity, Entity):
|
||||||
ini = char.dexterity.modifier
|
ini = entity.dexterity.modifier
|
||||||
if char.has_feature(QuickDraw):
|
if entity.has_feature(QuickDraw):
|
||||||
ini += char.proficiency_bonus
|
ini += entity.proficiency_bonus
|
||||||
if char.has_feature(DreadAmbusher):
|
if entity.has_feature(DreadAmbusher):
|
||||||
ini += char.wisdom.modifier
|
ini += entity.wisdom.modifier
|
||||||
if char.has_feature(RakishAudacity):
|
if entity.has_feature(RakishAudacity):
|
||||||
ini += char.charisma.modifier
|
ini += entity.charisma.modifier
|
||||||
|
|
||||||
has_advantage = (
|
has_advantage = (
|
||||||
char.has_feature(NaturalExplorerRevised)
|
entity.has_feature(NaturalExplorerRevised)
|
||||||
or char.has_feature(FeralInstinct)
|
or entity.has_feature(FeralInstinct)
|
||||||
or char.has_feature(AmbushMaster)
|
or entity.has_feature(AmbushMaster)
|
||||||
)
|
)
|
||||||
return ini, has_advantage
|
return ini, has_advantage
|
||||||
|
|
||||||
@@ -234,8 +230,8 @@ class NumericalInitiative:
|
|||||||
class Initiative(NumericalInitiative):
|
class Initiative(NumericalInitiative):
|
||||||
"""A character's initiative"""
|
"""A character's initiative"""
|
||||||
|
|
||||||
def __get__(self, char, Character):
|
def __get__(self, entity, Entity):
|
||||||
ini, has_advantage = super(Initiative, self).__get__(char, Character)
|
ini, has_advantage = super(Initiative, self).__get__(entity, Entity)
|
||||||
ini = "{:+d}".format(ini)
|
ini = "{:+d}".format(ini)
|
||||||
if has_advantage:
|
if has_advantage:
|
||||||
ini += "(A)"
|
ini += "(A)"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class TestCharacter(TestCase):
|
|||||||
self.assertEqual(char.spells[0].name, "my spell!")
|
self.assertEqual(char.spells[0].name, "my spell!")
|
||||||
|
|
||||||
def test_homebrew_infusions(self):
|
def test_homebrew_infusions(self):
|
||||||
char = Character(classes="artificer")
|
char = Character(class_list="artificer")
|
||||||
|
|
||||||
class MyInfusion(infusions.Infusion):
|
class MyInfusion(infusions.Infusion):
|
||||||
name = "my infusion!"
|
name = "my infusion!"
|
||||||
@@ -74,7 +74,7 @@ class TestCharacter(TestCase):
|
|||||||
self.assertIsInstance(char.infusions[0], infusions.Infusion)
|
self.assertIsInstance(char.infusions[0], infusions.Infusion)
|
||||||
self.assertEqual(char.infusions[0].name, "my infusion!")
|
self.assertEqual(char.infusions[0].name, "my infusion!")
|
||||||
# Pass a previously undefined infusion
|
# Pass a previously undefined infusion
|
||||||
char = Character(classes="artificer")
|
char = Character(class_list="artificer")
|
||||||
char.set_attrs(infusions=("spam_infusion",))
|
char.set_attrs(infusions=("spam_infusion",))
|
||||||
self.assertIsInstance(char.infusions[0], infusions.Infusion)
|
self.assertIsInstance(char.infusions[0], infusions.Infusion)
|
||||||
self.assertEqual(char.infusions[0].name, "Spam Infusion")
|
self.assertEqual(char.infusions[0].name, "Spam Infusion")
|
||||||
@@ -129,7 +129,7 @@ class TestCharacter(TestCase):
|
|||||||
self.assertEqual(repr(char), "<Wizard: Inara>")
|
self.assertEqual(repr(char), "<Wizard: Inara>")
|
||||||
|
|
||||||
def test_is_proficient(self):
|
def test_is_proficient(self):
|
||||||
char = Character(classes=["Wizard"])
|
char = Character(class_list=["Wizard"])
|
||||||
char.weapon_proficiencies
|
char.weapon_proficiencies
|
||||||
sword = Shortsword()
|
sword = Shortsword()
|
||||||
# Check for not-proficient weapon
|
# Check for not-proficient weapon
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from unittest import TestCase, skip
|
|
||||||
|
|
||||||
from dungeonsheets.armor import ChainShirt
|
|
||||||
from dungeonsheets.character import Character
|
|
||||||
from dungeonsheets.encounter import Encounter
|
|
||||||
from dungeonsheets.encounter.actions import Attack
|
|
||||||
from dungeonsheets.encounter.events import AttackEvent
|
|
||||||
from dungeonsheets.monsters import Monster, GiantRat
|
|
||||||
from dungeonsheets.stats import Ability
|
|
||||||
from dungeonsheets.dice import roll
|
|
||||||
|
|
||||||
|
|
||||||
class TestEncounter(TestCase):
|
|
||||||
"""Tests for features and feature-related activities."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
|
|
||||||
char = SimpleRanger()
|
|
||||||
|
|
||||||
class StravajiaxenAttack(Attack):
|
|
||||||
# TODO: Get default player attacks as a result of
|
|
||||||
# creating a player character and classes
|
|
||||||
|
|
||||||
def __init__(self, subj, obj):
|
|
||||||
super(StravajiaxenAttack, self).__init__(subj, obj)
|
|
||||||
|
|
||||||
def execute(self):
|
|
||||||
result = roll(20) + self.subj.dexterity.modifier + self.subj.proficiency_bonus
|
|
||||||
damage = roll(1, 8) + self.subj.dexterity.modifier
|
|
||||||
is_hit = result >= self.obj.armor_class
|
|
||||||
|
|
||||||
if is_hit:
|
|
||||||
self.obj.current_hp -= damage
|
|
||||||
|
|
||||||
return AttackEvent(self, result, damage, is_hit)
|
|
||||||
|
|
||||||
char.default_actions.append(StravajiaxenAttack)
|
|
||||||
|
|
||||||
class GiantRatAttack(Attack):
|
|
||||||
|
|
||||||
def __init__(self, subj, obj):
|
|
||||||
super(GiantRatAttack, self).__init__(subj, obj)
|
|
||||||
|
|
||||||
def execute(self):
|
|
||||||
result = roll(20) + 4
|
|
||||||
damage = roll(1, 4) + 2
|
|
||||||
is_hit = result >= self.obj.armor_class
|
|
||||||
|
|
||||||
if is_hit:
|
|
||||||
self.obj.current_hp -= damage
|
|
||||||
|
|
||||||
return AttackEvent(self, result, damage, is_hit)
|
|
||||||
|
|
||||||
enemy = GiantRat()
|
|
||||||
enemy.name = "Nameless Rat" # I don't want things to be personal...
|
|
||||||
enemy.default_actions.append(GiantRatAttack)
|
|
||||||
self.default_player = char
|
|
||||||
self.default_enemy = enemy
|
|
||||||
|
|
||||||
def test_encounter_rating(self):
|
|
||||||
battle = Encounter([self.default_player], [self.default_enemy])
|
|
||||||
self.assertRaises(NotImplementedError, battle.rating)
|
|
||||||
|
|
||||||
def test_opponents(self):
|
|
||||||
battle = Encounter([self.default_player], [self.default_enemy])
|
|
||||||
self.assertEqual([self.default_enemy], battle.opponents(self.default_player))
|
|
||||||
self.assertEqual([self.default_player], battle.opponents(self.default_enemy))
|
|
||||||
|
|
||||||
def test_allies(self):
|
|
||||||
battle = Encounter([self.default_player], [self.default_enemy])
|
|
||||||
self.assertEqual(0, len(battle.allies(self.default_player)))
|
|
||||||
self.assertEqual(0, len(battle.allies(self.default_enemy)))
|
|
||||||
|
|
||||||
def test_simple_rat_fight(self):
|
|
||||||
"""A fight against a giant rat"""
|
|
||||||
battle = Encounter([self.default_player], [self.default_enemy])
|
|
||||||
battle.reset()
|
|
||||||
results = battle.simulate()
|
|
||||||
for event in results:
|
|
||||||
print(str(event))
|
|
||||||
print(results)
|
|
||||||
|
|
||||||
@skip('NotImplementedError')
|
|
||||||
def test_langdedrosa_fight(self):
|
|
||||||
"""Can I run an encounter against Langdedrosa Cyanwrath?"""
|
|
||||||
char = Character()
|
|
||||||
char.set_attrs(name="Stravajiaxen")
|
|
||||||
char.set_attrs(weapons=["greataxe"])
|
|
||||||
char.set_attrs(armor="split mail")
|
|
||||||
|
|
||||||
# Check that race gets set to an object
|
|
||||||
char.set_attrs(race="half orc")
|
|
||||||
char.set_attrs(inspiration=False)
|
|
||||||
|
|
||||||
class LangdedrosaCyanwrath(Monster):
|
|
||||||
"""
|
|
||||||
**Action Surge (Recharges on a Short or Long Rest).** On his turn, Langdedrosa
|
|
||||||
can take one additional action.
|
|
||||||
|
|
||||||
**Improved Critical.** Langdedrosa's weapon attacks score a critical hit on a
|
|
||||||
roll of 19 or 20.
|
|
||||||
|
|
||||||
**Multiattack:** Schlangdedrosa attacks twice, either with his greatsword or spear.
|
|
||||||
|
|
||||||
**Greatsword.** Melee Weapon Attack: +6 to hit, reach 5 ft., one target.
|
|
||||||
Hit: 11 (2d6 + 4) slashing damage.
|
|
||||||
|
|
||||||
**Spear.** Melee or Ranged Weapon Attack: +6 to hit, reach 5 ft. or
|
|
||||||
ranged 20/60 ft., one target. Hit: 7 (1d6 + 4) piercing damage.
|
|
||||||
|
|
||||||
**Lightning Breath (Recharge 5-6)**. Schlangdedrosa breathes lightning in a
|
|
||||||
30-foot line that is 5 feet wide. Each creature in the line must make a DC 13
|
|
||||||
Dexterity saving throw, taking 22 (4d10) lightning damage on a failed save, or
|
|
||||||
half as much damage on a successful one.
|
|
||||||
|
|
||||||
**Climbing speed:** 30 ft.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = "Langdedrosa Cyanwrath"
|
|
||||||
description = "Medium humanoid (half-dragon), lawful evil"
|
|
||||||
challenge_rating = 4
|
|
||||||
armor_class = 17
|
|
||||||
skills = "Athletics +6, Intimidation +3, Perception +4"
|
|
||||||
senses = "blindsight 10 ft., darkvision 60ft., passive Perception 14"
|
|
||||||
strength = Ability(19)
|
|
||||||
dexterity = Ability(13)
|
|
||||||
constitution = Ability(16)
|
|
||||||
intelligence = Ability(10)
|
|
||||||
wisdom = Ability(14)
|
|
||||||
charisma = Ability(12)
|
|
||||||
speed = 30
|
|
||||||
swim_speed = 0
|
|
||||||
fly_speed = 0
|
|
||||||
hp_max = 57
|
|
||||||
hit_dice = "6d12+18"
|
|
||||||
|
|
||||||
lang = LangdedrosaCyanwrath()
|
|
||||||
|
|
||||||
battle = Encounter([char], [lang])
|
|
||||||
results = battle.simulate()
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleRanger(Character): # Taken from ranger2.py
|
|
||||||
"""This file describes the heroic adventurer Ranger2.
|
|
||||||
|
|
||||||
It's used primarily for saving characters from create-character,
|
|
||||||
where there will be many missing sections.
|
|
||||||
|
|
||||||
Modify this file as you level up and then re-generate the character
|
|
||||||
sheet by running ``makesheets`` from the command line.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
dungeonsheets_version = "0.9.4"
|
|
||||||
|
|
||||||
name = "Stravajiaxen"
|
|
||||||
player_name = "Ben"
|
|
||||||
|
|
||||||
# Be sure to list Primary class first
|
|
||||||
classes = ['Ranger'] # ex: ['Wizard'] or ['Rogue', 'Fighter']
|
|
||||||
levels = [3] # ex: [10] or [3, 2]
|
|
||||||
subclasses = ["Horizon Walker"] # ex: ['Necromacy'] or ['Thief', None]
|
|
||||||
background = "Uthgardt Tribe Member"
|
|
||||||
race = "Lizardfolk"
|
|
||||||
alignment = "Neutral good"
|
|
||||||
|
|
||||||
xp = 0
|
|
||||||
hp_max = 24
|
|
||||||
inspiration = 0 # integer inspiration value
|
|
||||||
|
|
||||||
# Ability Scores
|
|
||||||
strength = Ability(13)
|
|
||||||
dexterity = Ability(15)
|
|
||||||
constitution = Ability(12)
|
|
||||||
intelligence = Ability(8)
|
|
||||||
wisdom = Ability(15)
|
|
||||||
charisma = Ability(12)
|
|
||||||
|
|
||||||
# Select what skills you're proficient with
|
|
||||||
# ex: skill_proficiencies = ('athletics', 'acrobatics', 'arcana')
|
|
||||||
skill_proficiencies = ('athletics', 'insight', 'investigation')
|
|
||||||
|
|
||||||
# Any skills you have "expertise" (Bard/Rogue) in
|
|
||||||
skill_expertise = ()
|
|
||||||
|
|
||||||
# Named features / feats that aren't part of your classes, race, or background.
|
|
||||||
# Also include Eldritch Invocations and features you make multiple selection of
|
|
||||||
# (like Maneuvers for Fighter, Metamagic for Sorcerors, Trick Shots for
|
|
||||||
# Gunslinger, etc.)
|
|
||||||
# Example:
|
|
||||||
# features = ('Tavern Brawler',) # take the optional Feat from PHB
|
|
||||||
features = ()
|
|
||||||
|
|
||||||
# If selecting among multiple feature options: ex Fighting Style
|
|
||||||
# Example (Fighting Style):
|
|
||||||
# feature_choices = ('Archery',)
|
|
||||||
feature_choices = ('dueling',)
|
|
||||||
|
|
||||||
# Weapons/other proficiencies not given by class/race/background
|
|
||||||
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
|
|
||||||
_proficiencies_text = () # ex: ("thieves' tools",)
|
|
||||||
|
|
||||||
# Proficiencies and languages
|
|
||||||
languages = """Dwarvish, Common, Draconic"""
|
|
||||||
|
|
||||||
# Inventory
|
|
||||||
# TODO: Get yourself some money
|
|
||||||
cp = 0
|
|
||||||
sp = 0
|
|
||||||
ep = 0
|
|
||||||
gp = 0
|
|
||||||
pp = 0
|
|
||||||
|
|
||||||
# TODO: Put your equipped weapons and armor here
|
|
||||||
weapons = ('rapier', 'hand crossbow') # Example: ('shortsword', 'longsword')
|
|
||||||
magic_items = () # Example: ('ring of protection',)
|
|
||||||
armor = ChainShirt # Eg "leather armor"
|
|
||||||
shield = "" # Eg "shield"
|
|
||||||
|
|
||||||
equipment = """TODO: list the equipment and magic items your character carries"""
|
|
||||||
|
|
||||||
attacks_and_spellcasting = """TODO: Describe how your character usually attacks
|
|
||||||
or uses spells."""
|
|
||||||
|
|
||||||
# List of known spells
|
|
||||||
# Example: spells_prepared = ('magic missile', 'mage armor')
|
|
||||||
spells_prepared = () # Todo: Learn some spells
|
|
||||||
|
|
||||||
# Which spells have not been prepared
|
|
||||||
__spells_unprepared = ()
|
|
||||||
|
|
||||||
# all spells known
|
|
||||||
spells = spells_prepared + __spells_unprepared
|
|
||||||
|
|
||||||
# Wild shapes for Druid
|
|
||||||
wild_shapes = () # Ex: ('ape', 'wolf', 'ankylosaurus')
|
|
||||||
|
|
||||||
# Backstory
|
|
||||||
# Describe your backstory here
|
|
||||||
personality_traits = """TODO: How does your character behave? See the PHB for
|
|
||||||
examples of all the sections below"""
|
|
||||||
|
|
||||||
ideals = """TODO: What does your character believe in?"""
|
|
||||||
|
|
||||||
bonds = """TODO: Describe what debts your character has to pay,
|
|
||||||
and other commitments or ongoing quests they have."""
|
|
||||||
|
|
||||||
flaws = """TODO: Describe your characters interesting flaws.
|
|
||||||
"""
|
|
||||||
|
|
||||||
features_and_traits = """TODO: Describe other features and abilities your
|
|
||||||
character has."""
|
|
||||||
@@ -13,24 +13,24 @@ class TestMulticlass(TestCase):
|
|||||||
|
|
||||||
def test_constructor(self):
|
def test_constructor(self):
|
||||||
char = Character(
|
char = Character(
|
||||||
name="Multiclass", classes=["wizard", "fighter"], levels=[5, 4]
|
name="Multiclass", class_list=["wizard", "fighter"], levels=[5, 4]
|
||||||
)
|
)
|
||||||
self.assertIsInstance(char, Character)
|
self.assertIsInstance(char, Character)
|
||||||
|
|
||||||
def test_level(self):
|
def test_level(self):
|
||||||
char = Character(
|
char = Character(
|
||||||
name="Multiclass", classes=["wizard", "fighter"], levels=[5, 4]
|
name="Multiclass", class_list=["wizard", "fighter"], levels=[5, 4]
|
||||||
)
|
)
|
||||||
self.assertEqual(char.level, 9)
|
self.assertEqual(char.level, 9)
|
||||||
|
|
||||||
def test_spellcasting(self):
|
def test_spellcasting(self):
|
||||||
char = Character(
|
char = Character(
|
||||||
name="Multiclass", classes=["wizard", "fighter"], levels=[5, 4]
|
name="Multiclass", class_list=["wizard", "fighter"], levels=[5, 4]
|
||||||
)
|
)
|
||||||
self.assertEqual(len(char.spellcasting_classes), 1)
|
self.assertEqual(len(char.spellcasting_classes), 1)
|
||||||
char = Character(
|
char = Character(
|
||||||
name="Multiclass",
|
name="Multiclass",
|
||||||
classes=["wizard", "fighter"],
|
class_list=["wizard", "fighter"],
|
||||||
subclasses=[None, "Eldritch Knight"],
|
subclasses=[None, "Eldritch Knight"],
|
||||||
levels=[5, 4],
|
levels=[5, 4],
|
||||||
)
|
)
|
||||||
@@ -43,12 +43,12 @@ class TestMulticlass(TestCase):
|
|||||||
|
|
||||||
def test_proficiencies(self):
|
def test_proficiencies(self):
|
||||||
char1 = Character(
|
char1 = Character(
|
||||||
name="Multiclass", classes=["wizard", "fighter"], levels=[5, 4]
|
name="Multiclass", class_list=["wizard", "fighter"], levels=[5, 4]
|
||||||
)
|
)
|
||||||
for svt in ("intelligence", "wisdom"):
|
for svt in ("intelligence", "wisdom"):
|
||||||
self.assertIn(svt, char1.saving_throw_proficiencies)
|
self.assertIn(svt, char1.saving_throw_proficiencies)
|
||||||
char2 = Character(name="Multiclass", classes=["wizard", "rogue"], levels=[5, 4])
|
char2 = Character(name="Multiclass", class_list=["wizard", "rogue"], levels=[5, 4])
|
||||||
char3 = Character(name="Multiclass", classes=["rogue", "wizard"], levels=[4, 5])
|
char3 = Character(name="Multiclass", class_list=["rogue", "wizard"], levels=[4, 5])
|
||||||
sword = Shortsword()
|
sword = Shortsword()
|
||||||
self.assertTrue(char1.is_proficient(sword))
|
self.assertTrue(char1.is_proficient(sword))
|
||||||
# multiclassing into Rogue doesn't give simple weapon proficiency
|
# multiclassing into Rogue doesn't give simple weapon proficiency
|
||||||
|
|||||||
Reference in New Issue
Block a user