Added party XP thresholds to GM sheets.

This commit is contained in:
Mark Wolfman
2021-09-30 21:10:34 -05:00
parent 1d67463566
commit 5b8ef004c7
14 changed files with 152 additions and 20 deletions
+42
View File
@@ -0,0 +1,42 @@
from collections import namedtuple
XPThreshold = namedtuple("XPThreshold", ("easy", "medium", "hard", "deadly"))
xp_thresholds_by_character_level = {
1: XPThreshold(25, 50, 75, 100),
2: XPThreshold(50, 100, 150, 200),
3: XPThreshold(75, 150, 225, 400),
4: XPThreshold(125, 250, 375, 500),
5: XPThreshold(250, 500, 750, 1100),
6: XPThreshold(300, 600, 900, 1400),
7: XPThreshold(350, 750, 1100, 1700),
8: XPThreshold(450, 900, 1400, 2100),
9: XPThreshold(550, 1100, 1600, 2400),
10: XPThreshold(600, 1200, 1900, 2800),
11: XPThreshold(800, 1600, 2400, 3600),
12: XPThreshold(1000, 2000, 3000, 4500),
13: XPThreshold(1100, 2200, 3400, 5100),
14: XPThreshold(1250, 2500, 3800, 5700),
15: XPThreshold(1400, 2800, 4300, 6400),
16: XPThreshold(1600, 3200, 4800, 7200),
17: XPThreshold(2000, 3900, 5900, 8800),
18: XPThreshold(2100, 4200, 6300, 9500),
19: XPThreshold(2400, 4900, 7300, 10900),
20: XPThreshold(2800, 5700, 8500, 12700),
}
def xp_thresholds(party):
thresholds = []
for member in party:
xp_th = xp_thresholds_by_character_level.get(
getattr(member, 'level', 0), XPThreshold(0, 0, 0, 0))
thresholds.append(xp_th)
final_thresholds = XPThreshold(
easy=sum(th.easy for th in thresholds),
medium=sum(th.medium for th in thresholds),
hard=sum(th.hard for th in thresholds),
deadly=sum(th.deadly for th in thresholds),
)
return final_thresholds
+2
View File
@@ -4,6 +4,7 @@ from jinja2 import Environment, PackageLoader
from dungeonsheets.stats import mod_str, ability_mod_str, stat_abbreviation
from dungeonsheets.encounter import xp_thresholds
from dungeonsheets.monsters import challenge_rating_to_xp
@@ -35,4 +36,5 @@ def jinja_environment():
jinja_env.filters["ability_mod_str"] = ability_mod_str
jinja_env.filters["stat_abbreviation"] = stat_abbreviation
jinja_env.filters["challenge_rating_to_xp"] = challenge_rating_to_xp
jinja_env.filters["xp_thresholds"] = xp_thresholds
return jinja_env
@@ -84,7 +84,7 @@
<td>[[ ability.name | capitalize ]]</td>
<td>[[ ability.modifier | mod_str ]] ([[ ability.value ]])</td>
<td>[% if ability.name in character.saving_throw_proficiencies %]✓[% endif %]</td>
<td>[[ character.strength.saving_throw | mod_str ]]</td>
<td>[[ ability.saving_throw | mod_str ]]</td>
</tr>
[% endfor %]
</table>
@@ -53,4 +53,28 @@
[% endfor %]
</table>
<!-- XP thresholds for the party -->
<table>
<tr>
<th>&nbsp;</th>
<th>Easy</th>
<th>Medium</th>
<th>Hard</th>
<th>Deadly</th>
</tr>
<tr>
<th>XP Threshold</th>
[% for threshold in party | xp_thresholds %]
<td>[[ "{:,}".format(threshold) ]]</td>
[% endfor %]
</tr>
</table>
<dl>
[% for member in party %]
<dt>[[ member.name ]]</dt>
<dd>[[ member.languages ]]</dd>
[% endfor %]
</dl>
[% endif %]
+26 -2
View File
@@ -24,18 +24,27 @@
\\
[% endfor %]
\end{DndTable}
\begin{DndTable}{r c c c}
& AC & Pas.\ Per. & Spl.\ DC \\
\begin{DndTable}{r c c c c}
& AC & Pas.\ Per. & Max HP & Spl.\ DC \\
[% for member in party %]
[[ member.name[:28] ]]
& [[ member.armor_class ]]
& [[ member.perception.modifier + 10 ]]
& [[ member.max_hp ]]
& [% for class in member.class_list %]
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
[% endfor %]
\\
[% endfor %]
\end{DndTable}
%% XP thresholds for the party
\begin{DndTable}{r c c c c}
& Easy & Medium & Hard & Deadly \\
\textbf{XP Threshold} &
[% for threshold in party | xp_thresholds %]
[[ "{:,}".format(threshold) ]] [% if not loop.last %]&[% endif %]
[% endfor %]
\end{DndTable}
[% else %]
\begin{tabular}{r | c c c c c c}
& Str & Dex & Con & Int & Wis & Cha \\
@@ -64,6 +73,21 @@
\\
[% endfor %]
\end{tabular}
%% XP thresholds for the party
\begin{tabular}{r c c c c}
& Easy & Medium & Hard & Deadly \\
\textbf{XP Threshold} &
[% for threshold in party | xp_thresholds %]
[[ "{:,}".format(threshold) ]] [% if not loop.last %]&[% endif %]
[% endfor %]
\end{tabular}
[% endif %]
[% for member in party %]
\textbf{[[ member.name ]]}: [[ member.languages ]]
[% endfor %]
[% endif %]
@@ -1,4 +1,6 @@
[% if tables|length > 0 %]
<h1 id="gm-random-tables">Random Tables</h1>
[% endif %]
[% for table in tables %]
<h2 id="gm-random-tables-[[ table.name | to_heading_id ]]">[[ table.name ]]</h2>
@@ -1,5 +1,7 @@
[% if tables|length > 0 %]
\pdfbookmark[0]{Random Tables}{Random Tables}
\section*{Random Tables}
[% endif %]
[% for table in tables %]
\pdfbookmark[0]{[[ table.name ]]}{Random Table - [[ table.name ]]}
+7
View File
@@ -223,6 +223,13 @@ def make_gm_sheet(
# Add the party stats table and session summary
party = []
for char_file in gm_props.pop("party", []):
# Check if it's already resolved
if isinstance(char_file, Creature):
member = char_file
elif isinstance(char_file, type) and issubclass(char_file, Creature):
# Needs to be instantiated
member = char_file()
else:
# Resolve the file path
char_file = Path(char_file)
if not char_file.is_absolute():
+4 -2
View File
@@ -1,7 +1,7 @@
"""Convenience module holding base classes for the various kinds of
game mechanics."""
from dungeonsheets.content import Content
from dungeonsheets.content import Content, Creature
from dungeonsheets.spells import Spell
from dungeonsheets.features import Feature
from dungeonsheets.infusions import Infusion
@@ -14,5 +14,7 @@ from dungeonsheets.weapons import (
)
from dungeonsheets.armor import Armor, Shield
from dungeonsheets.magic_items import MagicItem
from dungeonsheets.monsters import Monster
from dungeonsheets.stats import Ability
from dungeonsheets.character import Character
from dungeonsheets.monsters import Monster
+3
View File
@@ -104,3 +104,6 @@ class Monster(Creature, metaclass=SpellFactory):
def is_beast(self):
is_beast = "beast" in self.description.lower()
return is_beast
def has_feature(self, *args, **kwargs):
return False
+3 -3
View File
@@ -1080,7 +1080,7 @@ class Stonemelder(Monster):
attacks. It knows the following sorcerer spells (an asterisked
spell is from *Princes of the Apocalypse* appendix B):
- Cantrips (at will): acid splash, blade ward, light mending, mold earth*
- Cantrips (at will): acid splash, blade ward, light, mending, mold earth*
- 1st level (4 slots): expeditious retreat, false life, shield
- 2nd level (3 slots): Maximilian's earthen grasp,* shatter
- 3rd level (3 slots): erupting earth,* meld into stone
@@ -1123,9 +1123,9 @@ class Stonemelder(Monster):
climb_speed = 0
hp_max = 75
hit_dice = "10d8 + 30"
spells = ["acid splash", "blade ward", "light mending", "mold earth*",
spells = ["acid splash", "blade ward", "light", "mending", "mold earth",
"expeditious retreat", "false life", "shield",
"Maximilian's earthen grasp*", "shatter" "erupting earth*",
"maximilian's earthen grasp", "shatter", "erupting earth",
"meld into stone", "stoneskin"]
+8 -1
View File
@@ -5,12 +5,19 @@ monsters, etc.
"""
from dungeonsheets import mechanics, monsters
dungeonsheets_version = "0.14.0"
sheet_type = "gm"
session_title = "Objects in Space"
party = ["rogue1.py", "paladin2.py"]
# Simple character definition
haryk_omanie = mechanics.Character(
name="Haryk Omanie",
)
party = ["rogue1.py", "paladin2.py", haryk_omanie, monsters.Veteran]
random_tables = ["conjure animals"]
+1 -1
View File
@@ -10,7 +10,7 @@ from dungeonsheets import mechanics
# This line (or one like it) is required in order for dungeonsheets to
# recognize the file.
dungeonsheets_version = "0.15.0"
dungeonsheets_version = "0.17.0"
# Specifying ``sheet_type = "gm"`` gives us GM notes instead of a
# player character sheet.
+17
View File
@@ -0,0 +1,17 @@
from unittest import TestCase
from dungeonsheets import stats, character, encounter
class TestStats(TestCase):
def new_character(self, level=1):
return character.Character(classes=['cleric'], levels=[level])
def test_xp_thresholds(self):
# One level 1 character
xp_th = encounter.xp_thresholds([self.new_character(1)])
self.assertEqual(xp_th, (25, 50, 75, 100))
# Three mixed-level characters
party = [self.new_character(9), self.new_character(8), self.new_character(6)]
xp_th = encounter.xp_thresholds(party)
self.assertEqual(xp_th, (1300, 2600, 3900, 5900))