core-book/lib/rpgmonster.sty
Joe bellus 7b33fef50e Initial commit
Initial project scaffolding
2025-03-10 18:21:53 -04:00

672 lines
21 KiB
TeX

\ExplSyntaxOn
\NewDocumentCommand {\RpgCRExp} { m }
{
\str_case_e:nnF {#1}
{
{0} {#1~(0~\xpname)}
{1/10} {0~(10~\xpname)}
{1/8} {#1~(25~\xpname)}
{1/4} {#1~(50~\xpname)}
{1/2} {#1~(100~\xpname)}
{1} {#1~(200~\xpname)}
{2} {#1~(450~\xpname)}
{3} {#1~(700~\xpname)}
{4} {#1~(\numprint{1100}~\xpname)}
{5} {#1~(\numprint{1800}~\xpname)}
{6} {#1~(\numprint{2300}~\xpname)}
{7} {#1~(\numprint{2900}~\xpname)}
{8} {#1~(\numprint{3900}~\xpname)}
{9} {#1~(\numprint{5000}~\xpname)}
{10} {#1~(\numprint{5900}~\xpname)}
{11} {#1~(\numprint{7200}~\xpname)}
{12} {#1~(\numprint{8400}~\xpname)}
{13} {#1~(\numprint{10000}~\xpname)}
{14} {#1~(\numprint{11500}~\xpname)}
{15} {#1~(\numprint{13000}~\xpname)}
{16} {#1~(\numprint{15000}~\xpname)}
{17} {#1~(\numprint{18000}~\xpname)}
{18} {#1~(\numprint{20000}~\xpname)}
{19} {#1~(\numprint{22000}~\xpname)}
{20} {#1~(\numprint{25000}~\xpname)}
{21} {#1~(\numprint{33000}~\xpname)}
{22} {#1~(\numprint{41000}~\xpname)}
{23} {#1~(\numprint{50000}~\xpname)}
{24} {#1~(\numprint{62000}~\xpname)}
{25} {#1~(\numprint{75000}~\xpname)}
{26} {#1~(\numprint{90000}~\xpname)}
{27} {#1~(\numprint{105000}~\xpname)}
{28} {#1~(\numprint{120000}~\xpname)}
{29} {#1~(\numprint{135000}~\xpname)}
{30} {#1~(\numprint{155000}~\xpname)}
}
{#1}
}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Monster environments
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Stat block made to look like the Monster Manual NPC definitions
\DeclareTColorBox {RpgMonsterNoBg} { O{} m }
{
enhanced,
frame~hidden,
boxrule = 0pt,
breakable,
parbox = false,
boxsep = 0pt,
toptitle = 3mm,
left = 0pt,
right = 0pt,
sharp~corners,
opacityback = 0,
colframe = titlered,
fonttitle = \RpgFontStatBlockTitle,
fontupper = \RpgFontStatBlockBody,
fontlower = \RpgFontStatBlockBody,
title = {#2},
coltitle = titlered,
#1
}
% Stat block made to look like stat blocks in the PHB.
\DeclareTColorBox {RpgMonsterBg} { O{} m }
{
enhanced,
frame~hidden,
before~skip = 12pt plus 3pt minus 3pt,
boxrule = 0pt,
breakable,
parbox = false,
boxsep = 2pt,
toptitle = 8pt,
top = 0pt,
left = 2pt,
right = 2pt,
bottom = 7pt,
sharp~corners,
oversize = 0pt,
borderline~north = {3pt} {0pt} {titlered},
borderline~north = {2pt} {0.5pt} {statblockribbon},
borderline~south = {3pt} {0pt} {titlered},
borderline~south = {2pt} {0.5pt} {statblockribbon},
colback = statblockbg,
colbacktitle = statblockbg,
colframe = titlered,
fonttitle = \RpgFontStatBlockTitle,
coltitle = titlered,
fontupper = \RpgFontStatBlockBody,
fontlower = \RpgFontStatBlockBody,
title = {#2},
#1
}
% Alias RpgMonster to correct background type
\bool_if:NTF \l__rpg_show_background_bool
{
\let\RpgMonster\RpgMonsterBg
\let\endRpgMonster\endRpgMonsterBg
}{
\let\RpgMonster\RpgMonsterNoBg
\let\endRpgMonster\endRpgMonsterNoBg
}
% Italicized text appearing immediately after monster's name
\NewDocumentCommand {\RpgMonsterType} {m}
{
\begin {hangingpar}
\textit {#1}
\end {hangingpar}
}
% Fancy DnD 5e stat block rule
\NewDocumentCommand {\RpgMonsterLine} {}
{
\par \vspace{-2pt} \noindent
\begin {tikzpicture}
\draw [ rulered, fill = rulered ] ( 0, 0 ) -- ( 0, 0.1 ) -- ( \linewidth, 0.05 );
\end {tikzpicture}
\par
}
% A description variant used to list creature attributes.
\newlist {__rpg_monster_attributes} {description} {1}
\setlist [__rpg_monster_attributes]
{
before = \color {titlered},
font = \RpgFontStatBlockBody,
labelsep = \l__rpg_space_dim,
nosep,
}
% Only prints the item label if the value was supplied
\cs_new_protected_nopar:Npn \__rpg_if_monster_attribute:nn #1#2
{
\tl_if_empty:NF {#1}
{ \item [#2] #1 }
}
%% Monster basics
\keys_define:nn { rpg / monster / basics }
{
armor-class .tl_set:N = \l__armor_class_tl,
armor-class .initial:n = 10,
armor-class .value_required:n = true,
armorclass .meta:n = { armor-class = {#1} },
hit-points .tl_set:N = \l__hit_points_tl,
hit-points .initial:n = \RpgDice {1d8},
hit-points .value_required:n = true,
hitpoints .meta:n = { hit-points = {#1} },
speed .tl_set:N = \l__speed_tl,
speed .initial:n = 30 ~ \unitsname,
speed .value_required:n = true,
}
\cs_new_protected_nopar:Npn \__rpg_monster_basics:
{
\begin {__rpg_monster_attributes}
\item [\armorclassname] \l__armor_class_tl
\item [\hitpointsname] \l__hit_points_tl
\item [\speedname] \l__speed_tl
\end {__rpg_monster_attributes}
}
\NewDocumentCommand {\RpgMonsterBasics} {o}
{
\group_begin:
\keys_set:nn { rpg / monster / basics } {#1}
\RpgMonsterLine
\__rpg_monster_basics:
\RpgMonsterLine
\group_end:
}
%% Monster CR/XP helper
% \l_tmpa_tl - CR
% \l_tmpb_tl - XP
\cs_new_protected_nopar:Npn \__rpg_cr_xp:N #1
{
\tl_set:Nn \l_tmpa_tl {#1}
\str_case_e:nnTF {#1}
{
{0} { \tl_set:Nn \l_tmpb_tl {0} }
{0/10}
{
\tl_set:Nn \l_tmpa_tl {0}
\tl_set:Nn \l_tmpb_tl {10}
}
{1/10}
{
\rpg@deprecate{ Challenge ~ 1/10 } {0.8} [ Use ~ challenge ~ 0/10 ~ instead. ]
\tl_set:Nn \l_tmpa_tl {0}
\tl_set:Nn \l_tmpb_tl {10}
}
{1/8} { \tl_set:Nn \l_tmpb_tl {25} }
{1/4} { \tl_set:Nn \l_tmpb_tl {50} }
{1/2} { \tl_set:Nn \l_tmpb_tl {100} }
{1} { \tl_set:Nn \l_tmpb_tl {200} }
{2} { \tl_set:Nn \l_tmpb_tl {450} }
{3} { \tl_set:Nn \l_tmpb_tl {700} }
{4} { \tl_set:Nn \l_tmpb_tl {1100} }
{5} { \tl_set:Nn \l_tmpb_tl {1800} }
{6} { \tl_set:Nn \l_tmpb_tl {2300} }
{7} { \tl_set:Nn \l_tmpb_tl {2900} }
{8} { \tl_set:Nn \l_tmpb_tl {3900} }
{9} { \tl_set:Nn \l_tmpb_tl {5000} }
{10} { \tl_set:Nn \l_tmpb_tl {5900} }
{11} { \tl_set:Nn \l_tmpb_tl {7200} }
{12} { \tl_set:Nn \l_tmpb_tl {8400} }
{13} { \tl_set:Nn \l_tmpb_tl {10000} }
{14} { \tl_set:Nn \l_tmpb_tl {11500} }
{15} { \tl_set:Nn \l_tmpb_tl {13000} }
{16} { \tl_set:Nn \l_tmpb_tl {15000} }
{17} { \tl_set:Nn \l_tmpb_tl {18000} }
{18} { \tl_set:Nn \l_tmpb_tl {20000} }
{19} { \tl_set:Nn \l_tmpb_tl {22000} }
{20} { \tl_set:Nn \l_tmpb_tl {25000} }
{21} { \tl_set:Nn \l_tmpb_tl {33000} }
{22} { \tl_set:Nn \l_tmpb_tl {41000} }
{23} { \tl_set:Nn \l_tmpb_tl {50000} }
{24} { \tl_set:Nn \l_tmpb_tl {62000} }
{25} { \tl_set:Nn \l_tmpb_tl {75000} }
{26} { \tl_set:Nn \l_tmpb_tl {90000} }
{27} { \tl_set:Nn \l_tmpb_tl {105000} }
{28} { \tl_set:Nn \l_tmpb_tl {120000} }
{29} { \tl_set:Nn \l_tmpb_tl {135000} }
{30} { \tl_set:Nn \l_tmpb_tl {155000} }
}
{ \l_tmpa_tl\ ( \numprint {\l_tmpb_tl} ~ \xpname ) }
{ #1 }
}
%% Monster CR/PB helper
% \l_tmpa_tl - CR
% \l_tmpb_tl - PB
\cs_new_protected_nopar:Npn \__rpg_cr_pb:N #1
{
\tl_set:Nn \l_tmpa_tl {#1}
\str_case_e:nnTF {#1}
{
{0} { \tl_set:Nn \l_tmpb_tl {+2} }
{1/8} { \tl_set:Nn \l_tmpb_tl {+2} }
{1/4} { \tl_set:Nn \l_tmpb_tl {+2} }
{1/2} { \tl_set:Nn \l_tmpb_tl {+2} }
{1} { \tl_set:Nn \l_tmpb_tl {+2} }
{2} { \tl_set:Nn \l_tmpb_tl {+2} }
{3} { \tl_set:Nn \l_tmpb_tl {+2} }
{4} { \tl_set:Nn \l_tmpb_tl {+2} }
{5} { \tl_set:Nn \l_tmpb_tl {+3} }
{6} { \tl_set:Nn \l_tmpb_tl {+3} }
{7} { \tl_set:Nn \l_tmpb_tl {+3} }
{8} { \tl_set:Nn \l_tmpb_tl {+3} }
{9} { \tl_set:Nn \l_tmpb_tl {+4} }
{10} { \tl_set:Nn \l_tmpb_tl {+4} }
{11} { \tl_set:Nn \l_tmpb_tl {+4} }
{12} { \tl_set:Nn \l_tmpb_tl {+4} }
{13} { \tl_set:Nn \l_tmpb_tl {+5} }
{14} { \tl_set:Nn \l_tmpb_tl {+5} }
{15} { \tl_set:Nn \l_tmpb_tl {+5} }
{16} { \tl_set:Nn \l_tmpb_tl {+5} }
{17} { \tl_set:Nn \l_tmpb_tl {+6} }
{18} { \tl_set:Nn \l_tmpb_tl {+6} }
{19} { \tl_set:Nn \l_tmpb_tl {+6} }
{20} { \tl_set:Nn \l_tmpb_tl {+6} }
{21} { \tl_set:Nn \l_tmpb_tl {+7} }
{22} { \tl_set:Nn \l_tmpb_tl {+7} }
{23} { \tl_set:Nn \l_tmpb_tl {+7} }
{24} { \tl_set:Nn \l_tmpb_tl {+7} }
{25} { \tl_set:Nn \l_tmpb_tl {+8} }
{26} { \tl_set:Nn \l_tmpb_tl {+8} }
{27} { \tl_set:Nn \l_tmpb_tl {+8} }
{28} { \tl_set:Nn \l_tmpb_tl {+8} }
{29} { \tl_set:Nn \l_tmpb_tl {+9} }
{30} { \tl_set:Nn \l_tmpb_tl {+9} }
}
{ \l_tmpb_tl }
{ #1 }
}
%% Monster details
\keys_define:nn { rpg / monster / details }
{
saving-throws .tl_set:N = \l__saving_throws_tl,
saving-throws .value_required:n = true,
savingthrows .meta:n = { saving-throws = {#1} },
skills .tl_set:N = \l__skills_tl,
skills .value_required:n = true,
damage-vulnerabilities .tl_set:N = \l__damage_vulnerabilities_tl,
damage-vulnerabilities .value_required:n = true,
damagevulnerabilities .meta:n = { damage-vulnerabilities = {#1} },
damage-resistances .tl_set:N = \l__damage_resistances_tl,
damage-resistances .value_required:n = true,
damageresistances .meta:n = { damage-resistances = {#1} },
damage-immunities .tl_set:N = \l__damage_immunities_tl,
damage-immunities .value_required:n = true,
damageimmunities .meta:n = { damage-immunities = {#1} },
condition-immunities .tl_set:N = \l__condition_immunities_tl,
condition-immunities .value_required:n = true,
conditionimmunities .meta:n = { condition-immunities = {#1} },
senses .tl_set:N = \l__senses_tl,
senses .initial:n = \defaultsensesname,
senses .value_required:n = true,
languages .tl_set:N = \l__languages_tl,
languages .initial:n = ---,
languages .value_required:n = true,
challenge .tl_set:N = \l__challenge_tl,
challenge .initial:n = 1,
challenge .value_required:n = true,
proficiency-bonus .tl_set:N = \l__proficiency_tl,
proficiency-bonus .value_required:n = false,
proficiency-bonus .default:n = *,
proficiencybonus .meta:n = { proficiency-bonus = {#1} },
}
\cs_new_protected_nopar:Npn \__rpg_monster_details:
{
\begin {__rpg_monster_attributes}
\__rpg_if_monster_attribute:nn {\l__saving_throws_tl} {\savesname}
\__rpg_if_monster_attribute:nn {\l__skills_tl} {\skillsname}
\__rpg_if_monster_attribute:nn {\l__damage_vulnerabilities_tl} {\dvulname}
\__rpg_if_monster_attribute:nn {\l__damage_resistances_tl} {\dresname}
\__rpg_if_monster_attribute:nn {\l__damage_immunities_tl} {\dimmname}
\__rpg_if_monster_attribute:nn {\l__condition_immunities_tl} {\cimmname}
\item [\sensesname] \l__senses_tl
\item [\languagesname] \l__languages_tl
\item [\challengename] \__rpg_cr_xp:N {\l__challenge_tl}
\tl_if_empty:NF {\l__proficiency_tl}
{
{\enit@svlabel{\enit@format{\pbonname}}}
\hskip \labelsep
\str_if_eq:VnTF \l__proficiency_tl *
{\__rpg_cr_pb:N {\l__challenge_tl}}
{\l__proficiency_tl}
}
\end {__rpg_monster_attributes}
}
\NewDocumentCommand {\RpgMonsterDetails} {o}
{
\group_begin:
\keys_set:nn { rpg / monster / details } {#1}
\RpgMonsterLine{}
\__rpg_monster_details:
\RpgMonsterLine{}
\group_end:
}
%% Ability scores
% Print ability score stats with auto-computed modifier
% e.g. \stat{12} prints "12 (+1)"
\cs_new_protected_nopar:Npn \__rpg_ability_score_modifier:N #1
{
\regex_match:NnTF \c__pos_int_regex {#1}
{
\int_set:Nn \l_tmpa_int { \fp_eval:n { floor ( ( #1 - 10 ) / 2 ) } }
#1 ~ (
\int_compare:nNnTF {\l_tmpa_int} < {0}
{ -- }
{+}
\int_abs:n \l_tmpa_int )
}
{#1}
}
\keys_define:nn { rpg / monster / ability_scores }
{
str .tl_set:N = \l__str_tl,
str .initial:n = 10,
str .value_required:n = true,
STR .meta:n = { str = #1 },
dex .tl_set:N = \l__dex_tl,
dex .initial:n = 10,
dex .value_required:n = true,
DEX .meta:n = { dex = #1 },
con .tl_set:N = \l__con_tl,
con .initial:n = 10,
con .value_required:n = true,
CON .meta:n = { con = #1 },
int .tl_set:N = \l__int_tl,
int .initial:n = 10,
int .value_required:n = true,
INT .meta:n = { int = #1 },
wis .tl_set:N = \l__wis_tl,
wis .initial:n = 10,
wis .value_required:n = true,
WIS .meta:n = { wis = #1 },
cha .tl_set:N = \l__cha_tl,
cha .initial:n = 10,
cha .value_required:n = true,
CHA .meta:n = { cha = #1 },
}
% Ability scores in a table
\cs_new_protected_nopar:Npn \__rpg_monster_ability_scores:
{
\color {titlered}
\par \vspace{1pt} \noindent
\begin{tabularx} {\linewidth} {YYYYYY}
\rule {0pt} {3.7mm} % adds space between hline and table
\textbf {\strstatname} &
\textbf {\dexstatname} &
\textbf {\constatname} &
\textbf {\intstatname} &
\textbf {\wisstatname} &
\textbf {\chastatname} \\
\exp_args:NV \__rpg_ability_score_modifier:N \l__str_tl &
\exp_args:NV \__rpg_ability_score_modifier:N \l__dex_tl &
\exp_args:NV \__rpg_ability_score_modifier:N \l__con_tl &
\exp_args:NV \__rpg_ability_score_modifier:N \l__int_tl &
\exp_args:NV \__rpg_ability_score_modifier:N \l__wis_tl &
\exp_args:NV \__rpg_ability_score_modifier:N \l__cha_tl
\end{tabularx}
\par \vspace{4pt}%
}
\NewDocumentCommand {\RpgMonsterAbilityScores} {o}
{
\group_begin:
\keys_set:nn { rpg / monster / ability_scores } {#1}
\__rpg_monster_ability_scores:
\group_end:
}
% Inline header for monster actions - similar to a paragraph
\NewDocumentCommand {\RpgMonsterAction} {m}
{ \par \smallskip \noindent \textsl { \textbf {#1.} } }
% Inline header for monster sub actions - similar to a subparagraph
\NewDocumentCommand {\RpgMonsterSubAction} {m}
{ \par \textsl { \textbf {#1.} } }
% Monster subsection header style
\NewDocumentCommand {\RpgMonsterSection} {m}
{
\addvspace{6pt plus 2pt minus 2pt} \noindent
\group_begin:
\RpgFontStatBlockSection #1\nopagebreak[4]
% \rule is a horizontal command, so placing it under the title incurs extra
% line spacing. Use \hrule (a vertical command) instead.
\vspace {2pt}\nopagebreak[4]
\hrule height 0.6pt
\group_end:
\par \vspace{5pt}
\noindent \ignorespaces
}
% Spellcasting levels
\newlist {RpgMonsterSpells} {description} {1}
\setlist [RpgMonsterSpells]
{
font = \normalfont \RpgFontStatBlockBody,
labelsep = \l__rpg_space_dim,
noitemsep,
topsep = 6pt plus 2pt minus 2pt,
}
\NewDocumentCommand {\RpgEmphSpellString} {m}
{
\group_begin:
\seq_set_from_clist:Nn \l_tmpa_seq {#1}
\seq_set_map:NNn \l_tmpb_seq \l_tmpa_seq { \exp_not:n { \emph {##1} } }
\seq_use:Nn \l_tmpb_seq { ,~ }
\group_end:
}
\NewDocumentCommand {\RpgInnateSpellLevel} { O {\innateatwillname} m }
{
\item
[
\regex_match:NnTF \c__pos_int_regex {#1}
{
\str_if_in:NnTF {#2} {,}
{ \__rpg_caption:nn {\numberperdayeachname} {#1} }
{ \__rpg_caption:nn {\numberperdayname} {#1} }
}
{#1}
:
]
\RpgEmphSpellString {#2}
}
\NewDocumentCommand {\RpgMonsterSpellLevel} { O{\spellcantripsname} O{\spellatwillname} m }
{
\item
[
\str_case_e:nnF {#1}
{
{1} {\spellfirstlevelname}
{2} {\spellsecondlevelname}
{3} {\spellthirdlevelname}
{4} {\spellfourthlevelname}
{5} {\spellfifthlevelname}
{6} {\spellsixthlevelname}
{7} {\spellseventhlevelname}
{8} {\spelleighthlevelname}
{9} {\spellninthlevelname}
}
{#1}
{~}(
\str_case_e:nnF {#2}
{
{1} {\spelloneslotname}
{2} {\spelltwoslotsname}
{3} {\spellthreeslotsname}
{4} {\spellfourslotsname}
}
{#2}
) :
]
\RpgEmphSpellString{#3}
}
%% Monster attacks
\tl_new:N \l__rpg_attack_distance_tl
\tl_new:N \l__rpg_attack_type_tl
\keys_define:nn { rpg / monster / attack }
{
name .tl_set:N = \l__name_tl,
name .value_required:n = true,
distance .choice:,
distance .choices:nn =
{ both, melee, ranged }
{ \tl_set:Nn \l__rpg_attack_distance_tl {\l_keys_choice_tl} },
distance .initial:n = both,
type .choice:,
type / weapon .code:n = { \tl_set:Nn \l__rpg_attack_type_tl {\weaponname} },
type / spell .code:n = { \tl_set:Nn \l__rpg_attack_type_tl {\spellname} },
type .initial:n = weapon,
mod .tl_set:N = \l__mod_tl,
mod .value_required:n = true,
reach .tl_set:N = \l__reach_tl,
reach .initial:n = 5,
reach .value_required:n = true,
range .tl_set:N = \l__range_tl,
range .initial:n = 20/60,
range .value_required:n = true,
targets .tl_set:N = \l__targets_tl,
targets .initial:n = \defaulttargetsname,
targets .value_required:n = true,
dmg .tl_set:N = \l__dmg_tl,
dmg .value_required:n = true,
dmg-type .tl_set:N = \l__dmg_type_tl,
dmg-type .value_required:n = true,
plus-dmg .tl_set:N = \l__plus_dmg_tl,
plus-dmg .value_required:n = true,
plus-dmg-type .tl_set:N = \l__plus_dmg_type_tl,
plus-dmg-type .value_required:n = true,
or-dmg .tl_set:N = \l__or_dmg_tl,
or-dmg .value_required:n = true,
or-dmg-when .tl_set:N = \l__or_dmg_when_tl,
or-dmg-when .value_required:n = true,
extra .tl_set:N = \l__extra_tl,
extra .value_required:n = true,
}
\cs_new_protected:Npn \__rpg_monster_reach:
{
\reachname\ \l__reach_tl\ \unitsname
}
\cs_new_protected:Npn \__rpg_monster_range:
{
\rangename\ \l__range_tl\ \unitsname
}
\cs_new_protected:Npn \__rpg_if_plus_dmg:
{
\tl_if_empty:NF {\l__plus_dmg_tl}
{ ~ \plusname\ \l__plus_dmg_tl\ \l__plus_dmg_type_tl\ \damagename }
}
\cs_new_protected_nopar:Npn \__rpg_if_or_dmg:
{
\tl_if_empty:NF {\l__or_dmg_tl}
{
, ~ \orname\ \l__or_dmg_tl\ \l__dmg_type_tl\ \damagename\ \l__or_dmg_when_tl
\tl_if_empty:NF {\l__plus_dmg_tl}
{,}
}
}
\cs_new_protected:Npn \__rpg_monster_attack:
{
\__rpg_check_for_key:Nnn \l__name_tl {\RpgMonsterAttack} {name}
\__rpg_check_for_key:Nnn \l__mod_tl {\RpgMonsterAttack} {mod}
\begin{RpgMonsterAction} {\l__name_tl}~
\str_case_e:nnF {\l__rpg_attack_distance_tl}
{
{ melee }
{
\textit{ \__rpg_caption:nn {\meleeattackname} {\l__rpg_attack_type_tl} : } ~ \__rpg_caption:nn {\tohitname} {\l__mod_tl}, ~ \__rpg_monster_reach:
}
{ ranged }
{
\textit{ \__rpg_caption:nn {\rangedattackname} {\l__rpg_attack_type_tl} : } ~ \__rpg_caption:nn {\tohitname} {\l__mod_tl}, ~ \__rpg_monster_range:
}
}
{% Melee and Ranged is the default
\textit{ \__rpg_caption:nn {\meleeorrangedattackname} {\l__rpg_attack_type_tl} : } ~ \__rpg_caption:nn {\tohitname} {\l__mod_tl}, ~ \__rpg_monster_reach:\ \orname\ \__rpg_monster_range:
}
, ~ \l__targets_tl. ~
\textit { \hitname : } ~
\str_if_empty:NF {\l__dmg_tl} % Don't show any damage if `dmg' is not set.
{
\l__dmg_tl\ \l__dmg_type_tl\ \damagename
\__rpg_if_or_dmg:
\__rpg_if_plus_dmg:
}
% `extra' is any special text that goes after the final damage; do not
% include the final full stop.
\l__extra_tl .
\end{RpgMonsterAction}
}
\NewDocumentCommand {\RpgMonsterAttack} {o}
{
\group_begin:
\keys_set:nn { rpg / monster / attack } {#1}
\__rpg_monster_attack:
\group_end:
}
\NewDocumentCommand {\RpgMonsterMelee} {o}
{
\group_begin:
\keys_set:nn { rpg / monster / attack } { #1, distance = melee }
\__rpg_monster_attack:
\group_end:
}
\NewDocumentCommand {\RpgMonsterRanged} {o}
{
\group_begin:
\keys_set:nn { rpg / monster / attack } { #1, distance = ranged }
\__rpg_monster_attack:
\group_end:
}
%% Legendary Actions
\newlist {RpgMonsterLegendaryActions} {description} {1}
\setlist [RpgMonsterLegendaryActions]
{
font = \RpgFontStatBlockBody,
labelsep = \l__rpg_space_dim,
noitemsep,
topsep = 6pt plus 2pt minus 2pt,
}
\NewDocumentCommand {\RpgMonsterLegendaryAction} { m m }
{
\item [ #1 . ] #2
}