mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
Added party XP thresholds to GM sheets.
This commit is contained in:
@@ -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
|
||||
@@ -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> </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 %]
|
||||
|
||||
@@ -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 ]]}
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user