mirror of
https://github.com/Nighthawk42/roll20-character-sheets.git
synced 2026-08-30 19:52:29 +00:00
3553 lines
141 KiB
HTML
3553 lines
141 KiB
HTML
<div class="sheet-sd">
|
|
<div class="sheet-section sheet-section-roll-whisper">
|
|
<div class="sd-row">
|
|
<label class="sheet-radio">
|
|
<input type="radio" name="attr_whispertoggle" value="" checked="checked">
|
|
<span title="@{whispertoggle} Public rolls">Public</span>
|
|
</label>
|
|
<label class="sheet-radio">
|
|
<input type="radio" name="attr_whispertoggle" value="/w gm ">
|
|
<span title="@{whispertoggle} Whisper rolls to GM">To GM</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-section sheet-section-roll-advantage">
|
|
<div class="sd-row">
|
|
<label class="sheet-radio">
|
|
<input type="radio" name="attr_advantagetoggle" value="{{query=1}} {{advantage=1}} {{r2=[[@{d20}">
|
|
<span title="@{advantagetoggle} Roll with advantage">Advantage</span>
|
|
</label>
|
|
<label class="sheet-radio">
|
|
<input type="radio" name="attr_advantagetoggle" value="{{query=1}} {{normal=1}} {{r2=[[0d20" checked="checked">
|
|
<span title="@{advantagetoggle} Straight rolls">Normal</span>
|
|
</label>
|
|
<label class="sheet-radio">
|
|
<input type="radio" name="attr_advantagetoggle" value="{{query=1}} {{disadvantage=1}} {{r2=[[@{d20}">
|
|
<span title="@{advantagetoggle} Roll with disadvantage">Disadvantage</span>
|
|
</label>
|
|
<div class="sheet-options-button" title="Sheet Options">
|
|
<input type="checkbox" name="attr_show_options"><span>⚙️</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input class="sheet-toggle-npc" name="attr_npc" type="hidden" value="0">
|
|
<div class="sheet-type-npc sheet-section sheet-section-npc-identity">
|
|
<h1>NPC</h1>
|
|
<div class="sd-row">
|
|
<div class="sd-col sd-core-stat">
|
|
<button type="roll" name="roll_initiative" title="Roll initiative; click token first"
|
|
value="@{wtype}&{template:simple} {{rname=Initiative}} {{mod=@{dexterity_mod}}} {{r1=[[@{initiative_style}+@{initiative_bonus}[INIT] &{tracker}]]}} {{isinit=1}} @{charname_output}">Name & Initiative</button>
|
|
<input type="text" class="sd-text" name="attr_character_name" title="@{character_name}" placeholder="Name">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label for="npc_level">Level</label>
|
|
<input type="text" class="sd-num" name="attr_base_level" title="@{base_level}" id="npc_level" value="1"
|
|
placeholder="level">
|
|
</div>
|
|
</div>
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label for="npc_movement">Movement</label>
|
|
<input type="text" class="sd-text" name="attr_movement" title="@{attr_movement}" id="npc_movement" value="Near"
|
|
placeholder="near, double near, etc.">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label for="npc_alignment">Alignment</label>
|
|
<input type="text" class="sd-text" name="attr_alignment" title="@{alignment}" id="npc_alignment" value="Chaotic"
|
|
placeholder="Chaotic, Lawful, Neutral">
|
|
</div>
|
|
</div>
|
|
<input class="sheet-toggle-npc-desc" name="attr_npc_showdesc" type="hidden" value="0">
|
|
<div class="sd-npc-desc">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label for="npc_description">Description</label>
|
|
<textarea class="sd-text" name="attr_description" title="@{description}" id="npc_description"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-type-pc sheet-section sheet-section-pc-identity">
|
|
<h1>Crawler</h1>
|
|
<div class="sd-row">
|
|
<div class="sd-col sd-core-stat">
|
|
<button type="roll" name="roll_initiative" title="Roll initiative; click token first"
|
|
value="@{wtype}&{template:simple} {{rname=Initiative}} {{mod=@{dexterity_mod}}} {{r1=[[@{initiative_style}+@{initiative_bonus}[INIT] &{tracker}]]}} {{isinit=1}} @{charname_output}">Name & Initiative</button>
|
|
<input type="text" class="sd-text" name="attr_character_name" title="@{character_name}" placeholder="Name">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label for="pc_ancestry">Ancestry</label>
|
|
<input type="text" class="sd-text" name="attr_ancestry" title="@{attr_ancestry}" id="pc_ancestry"
|
|
placeholder="Ancestry">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label for="pc_background">Background</label>
|
|
<input type="text" class="sd-text" name="attr_background" title="@{background}" id="pc_background"
|
|
placeholder="Background">
|
|
</div>
|
|
</div>
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label for="pc_class">Class</label>
|
|
<input type="text" class="sd-text" name="attr_class" title="@{class}" id="pc_class" placeholder="Class">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label for="pc_level">Level</span></label>
|
|
<input type="text" class="sd-num" name="attr_base_level" title="@{base_level}" id="pc_level" value="0">
|
|
</div>
|
|
<div class="sd-col">
|
|
<div>
|
|
<label for="pc_xp_current">XP (<span name="attr_lifetime_xp" title="@{lifetime_xp}">0</span> total)</label>
|
|
<input type="text" class="sd-num" name="attr_xp" title="@{xp}" value="0" id="pc_xp_current">
|
|
<span>/</span>
|
|
<span class="sd-num" name="attr_xp_nextlevel" title="@{xp_nextlevel}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label for="pc_title">Title</label>
|
|
<input type="text" class="sd-text" name="attr_title" title="@{title}" id="pc_title" placeholder="Title">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label for="pc_alignment">Alignment</label>
|
|
<input type="text" class="sd-text" name="attr_alignment" title="@{alignment}" id="pc_alignment" placeholder="Alignment">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label for="pc_deity">Deity</label>
|
|
<input type="text" class="sd-text" name="attr_deity" title="@{deity}" id="pc_deity" placeholder="Deity">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-type-npc sheet-section sheet-section-npc-stats">
|
|
<h1>NPC Stats</h1>
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<div class="sd-stat-item">
|
|
<label for="npc_hp_max" title="Hit Points">HP</label>
|
|
<input type="text" class="sd-num" name="attr_hp_max" title="@{hp_max}" id="npc_hp_max" value="1" placeholder="HP">
|
|
</div>
|
|
</div>
|
|
<div class="sd-col">
|
|
<div class="sd-stat-item">
|
|
<label for="npc_ac" title="Armor Class">AC</label>
|
|
<input type="text" class="sd-num" name="attr_ac" title="@{ac}" value="10" id="npc_ac" placeholder="AC">
|
|
<input type="text" class="sd-text armor-type" name="attr_armor" title="@{armor}" placeholder="Armor type" id="npc_armor">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sd-core-stats">
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_strength" title="Strength check"
|
|
value="@{wtype}&{template:simple} {{rname=Strength}} {{mod=@{strength_mod}}} {{r1=[[@{d20}+@{strength_mod}]]}} @{rtype}+@{strength_mod}]]}} @{charname_output}">STR</button>
|
|
<input type="text" class="sd-num" name="attr_strength_mod" title="@{strength_mod}" value="0">
|
|
</div>
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_dexterity" title="Dexterity check"
|
|
value="@{wtype}&{template:simple} {{rname=Dexterity}} {{mod=@{dexterity_mod}}} {{r1=[[@{d20}+@{dexterity_mod}]]}} @{rtype}+@{dexterity_mod}]]}} @{charname_output}">DEX</button>
|
|
<input type="text" class="sd-num" name="attr_dexterity_mod" title="@{dexterity_mod}" value="0">
|
|
</div>
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_constitution" title="Constitution check"
|
|
value="@{wtype}&{template:simple} {{rname=Constitution}} {{mod=@{constitution_mod}}} {{r1=[[@{d20}+@{constitution_mod}]]}} @{rtype}+@{constitution_mod}]]}} @{charname_output}">CON</button>
|
|
<input type="text" class="sd-num" name="attr_constitution_mod" title="@{constitution_mod}" value="0">
|
|
</div>
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_intelligence" title="Intelligence check"
|
|
value="@{wtype}&{template:simple} {{rname=Intelligence}} {{mod=@{intelligence_mod}}} {{r1=[[@{d20}+@{intelligence_mod}]]}} @{rtype}+@{intelligence_mod}]]}} @{charname_output}">INT</button>
|
|
<input type="text" class="sd-num" name="attr_intelligence_mod" title="@{intelligence_mod}" value="0">
|
|
</div>
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_wisdom" title="Wisdom check"
|
|
value="@{wtype}&{template:simple} {{rname=Wisdom}} {{mod=@{wisdom_mod}}} {{r1=[[@{d20}+@{wisdom_mod}]]}} @{rtype}+@{wisdom_mod}]]}} @{charname_output}">WIS</button>
|
|
<input type="text" class="sd-num" name="attr_wisdom_mod" title="@{wisdom_mod}" value="10">
|
|
</div>
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_charisma" title="Charisma check"
|
|
value="@{wtype}&{template:simple} {{rname=Charisma}} {{mod=@{charisma_mod}}} {{r1=[[@{d20}+@{charisma_mod}]]}} @{rtype}+@{charisma_mod}]]}} @{charname_output}">CHA</button>
|
|
<input type="text" class="sd-num" name="attr_charisma_mod" title="@{charisma_mod}" value="0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-type-pc sheet-section sheet-section-pc-stats">
|
|
<h1>
|
|
<span class="section-title">Stats</span>
|
|
<div class="luck-tokens">
|
|
<span>Luck</span>
|
|
<input type="checkbox" name="attr_lucktoken1" title="@{lucktoken1}">
|
|
<input class="sheet-toggle-pulp-mode" name="attr_pulp_mode" type="hidden" value="0">
|
|
<input type="checkbox" class="sheet-pulp-mode" name="attr_lucktoken2" title="@{lucktoken2}">
|
|
<input type="checkbox" class="sheet-pulp-mode" name="attr_lucktoken3" title="@{lucktoken3}">
|
|
<input type="checkbox" class="sheet-pulp-mode" name="attr_lucktoken4" title="@{lucktoken4}">
|
|
<input type="checkbox" class="sheet-pulp-mode" name="attr_lucktoken5" title="@{lucktoken5}">
|
|
<input type="checkbox" class="sheet-pulp-mode" name="attr_lucktoken6" title="@{lucktoken6}">
|
|
</div>
|
|
</h1>
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<div class="sd-stat-item">
|
|
<label for="pc_hp_current" title="Hit Points">HP</label>
|
|
<input type="text" class="sd-num" name="attr_hp" title="@{hp}" id="pc_hp_current" placeholder="HP">
|
|
<span>/</span>
|
|
<input type="text" class="sd-num" name="attr_hp_max" title="@{hp_max} id=" pc_hp_max" placeholder="Max">
|
|
</div>
|
|
</div>
|
|
<div class="sd-col">
|
|
<div class="sd-stat-item">
|
|
<label for="pc_ac" title="Armor Class">AC</label>
|
|
<input type="text" class="sd-num" name="attr_ac" title="@{ac}" value="10" id="pc_ac" placeholder="AC">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sd-core-stats">
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_strength" title="Strength check"
|
|
value="@{wtype}&{template:simple} {{rname=Strength}} {{mod=@{strength_mod}}} {{r1=[[@{d20}+@{strength_mod}]]}} @{rtype}+@{strength_mod}]]}} @{charname_output}">STR</button>
|
|
<input type="text" class="sd-num" name="attr_strength_base" title="@{strength}" value="10" min="1" max="99">
|
|
<span class="sd-num" name="attr_strength_mod" title="@{strength_mod}">0</span>
|
|
<input type="hidden" name="attr_strength" value="10">
|
|
</div>
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_dexterity" title="Dexterity check"
|
|
value="@{wtype}&{template:simple} {{rname=Dexterity}} {{mod=@{dexterity_mod}}} {{r1=[[@{d20}+@{dexterity_mod}]]}} @{rtype}+@{dexterity_mod}]]}} @{charname_output}">DEX</button>
|
|
<input type="text" class="sd-num" name="attr_dexterity_base" title="@{dexterity}" value="10" min="1" max="99">
|
|
<span class="sd-num" name="attr_dexterity_mod" title="@{dexterity_mod}">0</span>
|
|
<input type="hidden" name="attr_dexterity" value="10">
|
|
</div>
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_constitution" title="Constitution check"
|
|
value="@{wtype}&{template:simple} {{rname=Constitution}} {{mod=@{constitution_mod}}} {{r1=[[@{d20}+@{constitution_mod}]]}} @{rtype}+@{constitution_mod}]]}} @{charname_output}">CON</button>
|
|
<input type="text" class="sd-num" name="attr_constitution_base" title="@{constitution}" value="10" min="1"
|
|
max="99">
|
|
<span class="sd-num" name="attr_constitution_mod" title="@{constitution_mod}">0</span>
|
|
<input type="hidden" name="attr_constitution" value="10">
|
|
</div>
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_intelligence" title="Intelligence check"
|
|
value="@{wtype}&{template:simple} {{rname=Intelligence}} {{mod=@{intelligence_mod}}} {{r1=[[@{d20}+@{intelligence_mod}]]}} @{rtype}+@{intelligence_mod}]]}} @{charname_output}">INT</button>
|
|
<input type="text" class="sd-num" name="attr_intelligence_base" title="@{intelligence}" value="10" min="1"
|
|
max="99">
|
|
<span class="sd-num" name="attr_intelligence_mod" title="@{intelligence_mod}">0</span>
|
|
<input type="hidden" name="attr_intelligence" value="10">
|
|
</div>
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_wisdom" title="Wisdom check"
|
|
value="@{wtype}&{template:simple} {{rname=Wisdom}} {{mod=@{wisdom_mod}}} {{r1=[[@{d20}+@{wisdom_mod}]]}} @{rtype}+@{wisdom_mod}]]}} @{charname_output}">WIS</button>
|
|
<input type="text" class="sd-num" name="attr_wisdom_base" title="@{wisdom}" value="10" min="1" max="99">
|
|
<span class="sd-num" name="attr_wisdom_mod" title="@{wisdom_mod}">0</span>
|
|
<input type="hidden" name="attr_wisdom" value="10">
|
|
</div>
|
|
<div class="sd-core-stat sd-stat-item">
|
|
<button type="roll" name="roll_charisma" title="Charisma check"
|
|
value="@{wtype}&{template:simple} {{rname=Charisma}} {{mod=@{charisma_mod}}} {{r1=[[@{d20}+@{charisma_mod}]]}} @{rtype}+@{charisma_mod}]]}} @{charname_output}">CHA</button>
|
|
<input type="text" class="sd-num" name="attr_charisma_base" title="@{charisma}" value="10" min="1" max="99">
|
|
<span class="sd-num" name="attr_charisma_mod" title="@{charisma_mod}">0</span>
|
|
<input type="hidden" name="attr_charisma" value="10">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-section sheet-section-actions">
|
|
<h1>Attacks & Spells</h1>
|
|
<div>
|
|
<fieldset class="repeating_attack">
|
|
<div class="sheet-fieldset-item">
|
|
<div class="sheet-checkbox-cog" title="Edit Details">
|
|
<input type="checkbox" name="attr_options-flag" checked="checked"><span></span>
|
|
<span class="sheet-toggle-icon"></span>
|
|
</div>
|
|
<input type="checkbox" name="attr_options-flag" checked="checked" class="sheet-toggle" hidden>
|
|
<div class="sheet-toggle-checked">
|
|
<div class="sheet-form">
|
|
<div class="sheet-form-body">
|
|
<h1>Attack or Spell Details
|
|
<div class="sheet-checkbox-cog" title="Close Form">
|
|
<input type="checkbox" name="attr_options-flag" checked="checked"><span></span>
|
|
<span class="sheet-toggle-icon"></span>
|
|
</div>
|
|
<button type="roll" name="roll_attack" value="@{rollbase}" class="sheet-attack-button" title="Use action in chat">Test Roll</button>
|
|
</h1>
|
|
<div class="sheet-form-group">
|
|
<label for="attr_atkname">Name of Attack, Weapon, or Spell:</label>
|
|
<input type="text" name="attr_atkname" id="attr_atkname" value="Punch" placeholder="+2 Sentient Scythe of the Swinefolk">
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label>Roll Stat:</label>
|
|
<select name="attr_atkattr_base">
|
|
<option value="@{strength_mod}" selected="selected">STR</option>
|
|
<option value="@{dexterity_mod}">DEX</option>
|
|
<option value="@{constitution_mod}">CON</option>
|
|
<option value="@{intelligence_mod}">INT</option>
|
|
<option value="@{wisdom_mod}">WIS</option>
|
|
<option value="@{charisma_mod}">CHA</option>
|
|
<option value="0">-</option>
|
|
</select>
|
|
</div>
|
|
<div class="sd-col">
|
|
<label>Attack Bonus:</label>
|
|
<input type="text" name="attr_atkmod" placeholder="0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label>Spell Duration:</label>
|
|
<input type="text" name="attr_duration" placeholder="Instant">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label>DC to Cast:</label>
|
|
<input type="text" name="attr_castdc" placeholder="15">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label>Attack / Spell Range:</label>
|
|
<input type="text" name="attr_atkrange" placeholder="Near">
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label>Magic Bonus to attacks & damage:</label>
|
|
<input type="text" name="attr_atkmagic" placeholder="0">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label for="attr_atkcritrange">Minimum Die Roll To Crit:</label>
|
|
<input type="text" id="attr_atkcritrange" name="attr_atkcritrange" value="20" placeholder="20">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label class="sheet-form-checkbox">
|
|
<input type="checkbox" name="attr_dmgflag" value="{{damage=1}} {{dmg1flag=1}}" checked>
|
|
<span>Apply Damage Type 1</span>
|
|
</label>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label for="attr_dmgbase">Damage Roll:</label>
|
|
<input type="text" id="attr_dmgbase" name="attr_dmgbase" placeholder="1d6">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label for="attr_dmgmod">Damage Bonus:</label>
|
|
<input type="text" name="attr_dmgmod" id="attr_dmgmod" placeholder="0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label for="attr_dmgcustcrit">Custom Critical Damage (blank for 2x roll):</label>
|
|
<input type="text" id="attr_dmgcustcrit" name="attr_dmgcustcrit" placeholder="3d6 [Slashing] + 1d12 [Necrotic]">
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label class="sheet-form-checkbox">
|
|
<input type="checkbox" name="attr_dmg2flag" value="{{damage=1}} {{dmg2flag=1}}">
|
|
<span>Apply Damage Type 2</span>
|
|
</label>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label for="attr_dmg2base">Damage Roll:</label>
|
|
<input type="text" id="attr_dmg2base" name="attr_dmg2base" placeholder="2d6 [Poison]">
|
|
</div>
|
|
<div class="sd-col">
|
|
<label for="attr_dmg2mod">Damage Bonus:</label>
|
|
<input type="text" id="attr_dmg2mod" name="attr_dmg2mod" placeholder="0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label>Custom Critical Damage (blank for 2x roll):</label>
|
|
<input type="text" name="attr_dmg2custcrit" placeholder="12d8 [Fire]">
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label class="sheet-form-checkbox">
|
|
<input type="checkbox" name="attr_equipped" checked="checked" value="1">
|
|
<span>Equipped; Ready to Use</span>
|
|
</label>
|
|
</div>
|
|
<div class="sd-col">
|
|
<label class="sheet-form-checkbox">
|
|
<input type="checkbox" name="attr_fixed" checked="checked" value="1">
|
|
<span>Repaired; Reconciled</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label>Description or Flavor Text:</label>
|
|
<textarea name="attr_atk_desc" class="attr_atk_desc" placeholder="Swings the blade in a wide arc!"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-toggle-unchecked">
|
|
<div class="sd-attacks">
|
|
<input type="text" name="attr_atkbonus" class="sheet-hidden" value="">
|
|
<input type="text" name="attr_atkdmgtype" class="sheet-hidden" value="">
|
|
<input type="checkbox" name="attr_equipped" class="atk-equipped" value="1" title="Equipped / Usable">
|
|
<input type="checkbox" name="attr_fixed" class="atk-fixed" value="1" title="Fixed; Reconciled or Broken; Forbidden">
|
|
<button type="roll" name="roll_attack" value="@{rollbase}" class="sheet-attack-button" title="Use action in chat">
|
|
<span name="attr_atkname"></span>
|
|
(<span name="attr_atkbonus"></span>)
|
|
<span name="attr_atkdmgtype"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" name="attr_rollbase">
|
|
<input type="hidden" name="attr_rollbase_dmg">
|
|
<input type="hidden" name="attr_rollbase_crit">
|
|
<input type="hidden" name="attr_hldmg">
|
|
<button type="roll" name="roll_attack_dmg" value="@{rollbase_dmg}" class="sheet-hidden"></button>
|
|
<button type="roll" name="roll_attack_crit" value="@{rollbase_crit}" class="sheet-hidden"></button>
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
<input class="sheet-toggle-counters" name="attr_show_counters" type="hidden" value="0">
|
|
<div class="sheet-section sheet-section-counters">
|
|
<h1>Counters</h1>
|
|
<div>
|
|
<fieldset class="repeating_counter">
|
|
<div class="custom-counter-item">
|
|
<div class="counter_head">
|
|
<input type="number" name="attr_counter" placeholder="0" title="Current value" value="0">
|
|
<span>/</span>
|
|
<input type="text" class="sd-num" name="attr_counter_max" placeholder="Max" title="Maximum value" value="">
|
|
</div>
|
|
<div class="counter_footer">
|
|
<input type="text" name="attr_title" maxlength="60" value="Custom counter" placeholder="Daily Ability" title="Counter title">
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-section sheet-section-features">
|
|
<h1>Talents, Traits, Abilities, Notes</h1>
|
|
<div>
|
|
<fieldset class="repeating_trait">
|
|
<div class="sheet-fieldset-item">
|
|
<div class="sheet-checkbox-cog" title="Edit Details">
|
|
<input type="checkbox" name="attr_options-flag" checked="checked">
|
|
<span class="sheet-toggle-icon"></span>
|
|
</div>
|
|
<input type="checkbox" name="attr_options-flag" checked="checked" class="sheet-toggle" hidden>
|
|
<div class="sheet-toggle-checked">
|
|
<div class="sheet-form">
|
|
<div class="sheet-form-body">
|
|
<h1>Define a Talent, Trait, Ability, or open-ended Notes
|
|
<div class="sheet-checkbox-cog" title="Close Form">
|
|
<input type="checkbox" name="attr_options-flag" checked="checked"><span></span>
|
|
<span class="sheet-toggle-icon"></span>
|
|
</div>
|
|
<button type="roll" name="roll_output" title="Share feature in chat" value="@{wtype}&{template:traits} @{charname_output} {{name=@{name}}} {{description=@{description}}}">Test Output</button>
|
|
</h1>
|
|
<div class="sheet-form-group">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label for="attr_name">
|
|
<p>Title:</p>
|
|
<input type="text" name="attr_name" id="attr_name" class="trait-name" maxlength="60">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label>
|
|
<p>Description:</p>
|
|
<textarea name="attr_description" class="trait-desc"></textarea>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label class="sheet-form-checkbox">
|
|
<input type="checkbox" name="attr_hide_details" value="1">
|
|
<span>Hide Description in List View</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-toggle-unchecked">
|
|
<div class="sheet-feature">
|
|
<button type="roll" name="roll_output" title="Share feature in chat"
|
|
value="@{wtype}&{template:traits} @{charname_output} {{name=@{name}}} {{description=@{description}}}">
|
|
<div class="sheet-feature-name">
|
|
<span name="attr_name"></span>
|
|
</div>
|
|
<input type="hidden" name="attr_hide_details" class="sheet-field-untoggle">
|
|
<div class="sheet-feature-details">
|
|
<span name="attr_description"></span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-type-pc sheet-section sheet-section-pc-gear">
|
|
<h1>Gear</h1>
|
|
<div class="sd-row">
|
|
<div class="sd-col sd-coinage">
|
|
<label><span>GP</span><input type="text" class="sd-num" name="attr_gp" title="@{gp}" value="0"></label>
|
|
</div>
|
|
<div class="sd-col sd-coinage">
|
|
<label><span>SP</span><input type="text" class="sd-num" name="attr_sp" title="@{sp}" value="0"></label>
|
|
</div>
|
|
<div class="sd-col sd-coinage">
|
|
<label><span>CP</span><input type="text" class="sd-num" name="attr_cp" title="@{cp}" value="0"></label>
|
|
</div>
|
|
<div class="sd-col">
|
|
<input type="hidden" class="sd-num" name="attr_slots" title="@{slots}" id="pc_slots_current" value="0">
|
|
<label for="pc_slots_total">
|
|
<span>Slots</span>
|
|
<input type="text" class="sd-num" name="attr_slots_total" title="@{slots_total}" id="pc_slots_total" value="10">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="sd-gear-slots">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label><span>1.</span><input type="hidden" name="attr_gearslot1enabled" value="1"><input type="text"
|
|
class="sd-text gearslot1" name="attr_gearslot1" title="@{gearslot1}"></label>
|
|
<label><span>2.</span><input type="hidden" name="attr_gearslot2enabled" value="1"><input type="text"
|
|
class="sd-text" name="attr_gearslot2" title="@{gearslot2}"></label>
|
|
<label><span>3.</span><input type="hidden" name="attr_gearslot3enabled" value="1"><input type="text"
|
|
class="sd-text" name="attr_gearslot3" title="@{gearslot3}"></label>
|
|
<label><span>4.</span><input type="hidden" name="attr_gearslot4enabled" value="1"><input type="text"
|
|
class="sd-text" name="attr_gearslot4" title="@{gearslot4}"></label>
|
|
<label><span>5.</span><input type="hidden" name="attr_gearslot5enabled" value="1"><input type="text"
|
|
class="sd-text" name="attr_gearslot5" title="@{gearslot5}"></label>
|
|
<label><span>6.</span><input type="hidden" name="attr_gearslot6enabled" value="1"><input type="text"
|
|
class="sd-text" name="attr_gearslot6" title="@{gearslot6}"></label>
|
|
<label><span>7.</span><input type="hidden" name="attr_gearslot7enabled" value="1"><input type="text"
|
|
class="sd-text" name="attr_gearslot7" title="@{gearslot7}"></label>
|
|
<label><span>8.</span><input type="hidden" name="attr_gearslot8enabled" value="1"><input type="text"
|
|
class="sd-text" name="attr_gearslot8" title="@{gearslot8}"></label>
|
|
<label><span>9.</span><input type="hidden" name="attr_gearslot9enabled" value="1"><input type="text"
|
|
class="sd-text" name="attr_gearslot9" title="@{gearslot9}"></label>
|
|
<label><span>10.</span><input type="hidden" name="attr_gearslot10enabled" value="1"><input type="text"
|
|
class="sd-text" name="attr_gearslot10" title="@{gearslot10}"></label>
|
|
</div>
|
|
<div class="sd-col">
|
|
<label><span>11.</span><input type="hidden" name="attr_gearslot11enabled" value="0"><input type="text"
|
|
class="sd-text" name="attr_gearslot11" title="@{gearslot11}"></label>
|
|
<label><span>12.</span><input type="hidden" name="attr_gearslot12enabled" value="0"><input type="text"
|
|
class="sd-text" name="attr_gearslot12" title="@{gearslot12}"></label>
|
|
<label><span>13.</span><input type="hidden" name="attr_gearslot13enabled" value="0"><input type="text"
|
|
class="sd-text" name="attr_gearslot13" title="@{gearslot13}"></label>
|
|
<label><span>14.</span><input type="hidden" name="attr_gearslot14enabled" value="0"><input type="text"
|
|
class="sd-text" name="attr_gearslot14" title="@{gearslot14}"></label>
|
|
<label><span>15.</span><input type="hidden" name="attr_gearslot15enabled" value="0"><input type="text"
|
|
class="sd-text" name="attr_gearslot15" title="@{gearslot15}"></label>
|
|
<label><span>16.</span><input type="hidden" name="attr_gearslot16enabled" value="0"><input type="text"
|
|
class="sd-text" name="attr_gearslot16" title="@{gearslot16}"></label>
|
|
<label><span>17.</span><input type="hidden" name="attr_gearslot17enabled" value="0"><input type="text"
|
|
class="sd-text" name="attr_gearslot17" title="@{gearslot17}"></label>
|
|
<label><span>18.</span><input type="hidden" name="attr_gearslot18enabled" value="0"><input type="text"
|
|
class="sd-text" name="attr_gearslot18" title="@{gearslot18}"></label>
|
|
<label><span>19.</span><input type="hidden" name="attr_gearslot19enabled" value="0"><input type="text"
|
|
class="sd-text" name="attr_gearslot19" title="@{gearslot19}"></label>
|
|
<label><span>20.</span><input type="hidden" name="attr_gearslot20enabled" value="0"><input type="text"
|
|
class="sd-text" name="attr_gearslot20" title="@{gearslot20}"></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sd-gear-noslots-screen">
|
|
<div class="sd-row">
|
|
<div class="sd-col">
|
|
<label>
|
|
<div>
|
|
<span>Other Gear, Personal Treasure, etc.</span>
|
|
</div>
|
|
<div>
|
|
<textarea class="sd-text" name="attr_gearnoslots" title="@{gearnoslots}"></textarea>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sd-gear-noslots-print">
|
|
<h1>Other Gear, Personal Treasure, etc.</h1>
|
|
<span name="attr_gearnoslots"></span>
|
|
</div>
|
|
<div class="sheet-section sheet-toggler-options">
|
|
<input type="checkbox" name="attr_show_options" class="sheet-toggle" hidden>
|
|
<div class="sheet-toggle-checked">
|
|
<div class="sheet-form">
|
|
<div class="sheet-form-body">
|
|
<h1>Sheet Options
|
|
<div class="sheet-checkbox-cog" title="Close Options">
|
|
<input type="checkbox" name="attr_show_options"><span></span>
|
|
<span class="sheet-toggle-icon"></span>
|
|
</div>
|
|
</h1>
|
|
<div class="sheet-form-group">
|
|
<label class="sheet-form-checkbox" title="@{npc}">
|
|
<input type="checkbox" name="attr_npc" value="1">
|
|
<span>NPC Sheet</span>
|
|
</label>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label class="sheet-form-checkbox" title="@{npc_showdesc}">
|
|
<input type="checkbox" name="attr_npc_showdesc" value="1">
|
|
<span>Show NPC Description</span>
|
|
</label>
|
|
</div>
|
|
<div class="sheet-form-group" title="@{dtype}">
|
|
<label>Auto Damage Roll
|
|
<select name="attr_dtype" style="max-width: 200px">
|
|
<option value="full">Auto Roll Damage & Crit</option>
|
|
<option value="pick">Don't Auto Roll Damage</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label class="sheet-form-checkbox" title="@{pulp_mode}">
|
|
<input type="checkbox" name="attr_pulp_mode" value="1">
|
|
<span>Pulp Mode (Show Multiple Luck Tokens)</span>
|
|
</label>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label class="sheet-form-checkbox" title="@{show_counters}">
|
|
<input type="checkbox" name="attr_show_counters" value="1">
|
|
<span>Show Custom Resource Counters</span>
|
|
</label>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label class="sheet-form-checkbox">
|
|
<input type="checkbox" name="attr_init_tiebreaker" value="1">
|
|
<span>Add Dex Tiebreaker to Initiative</span>
|
|
</label>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label>Core Die Roll:</label>
|
|
<input type="text" name="attr_core_die" value="1d20" placeholder="1d20" title="@{core_die}" style="max-width: 100px;" maxlength="20">
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<p>IMPORT CHARACTERS AND MONSTERS</p>
|
|
<p>Import Monsters from the book or import PCs from Shadowdarklings. To import:</p>
|
|
<ol>
|
|
<li>Copy and paste (a) a monster stat block from the official Shadowdark PDF or (b) exported Shadowdarklings JSON below.</li>
|
|
<li>Click the checkbox to confirm. This additional step prevents accidental overwrites.</li>
|
|
<li>Click the button to Import JSON or Monster Text. Warning: This will overwrite existing data but will not clear attacks and spells, features, or custom counters.</li>
|
|
</ol>
|
|
<p>Most class traits and mods are not applied automatically due to license restrictions. Review and correct your sheet after importing.</p>
|
|
<label>JSON (Shadowdarklings format) or Monster Text (Core rules PDF format)</label>
|
|
<textarea class="sd-text" name="attr_json" title="@{json}"></textarea>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label class="sheet-form-checkbox" title="@{sheetwipe_confirm_flag}">
|
|
<input type="checkbox" name="attr_sheetwipe_confirm_flag" value="1">
|
|
<span>Confirm: This import will replace stats</span>
|
|
</label>
|
|
<div>
|
|
<button type="action" name="act_importjson">Import Shadowdarklings JSON</button>
|
|
<button type="action" name="act_importmonster">Import Monster Text from Book</button>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-form-group">
|
|
<label>Version:</label>
|
|
<input type="text" name="attr_version" value="2025.12.11.0" hidden disabled>
|
|
<span name="attr_version"></span>
|
|
</div>
|
|
<div>
|
|
<span>Roll20 Character Sheet for Shadowdark RPG</span>
|
|
<div class="legal">
|
|
<p>LEGAL:</p>
|
|
<p>
|
|
Roll20 Character Sheet for Shadowdark RPG is an independent product published under the Shadowdark RPG
|
|
Third-Party License and is not affiliated with The Arcane Library, LLC. Shadowdark RPG © 2023 The Arcane
|
|
Library, LLC.
|
|
</p>
|
|
<p>
|
|
This is a community-made and community-supported sheet. Have fun, and use at your own risk!
|
|
</p>
|
|
<p>CREDIT AND THANKS:</p>
|
|
<ul class="sd-credits">
|
|
<li>Giffyglyph for creating the 5e Darker Dungeons sheet which served as the basis for this sheet.</li>
|
|
<li>Kelsey for creating Shadowdark RPG, and the entire Shadowdark RPG community for supporting the
|
|
game, Aritz and Herpopotamus for testing and motivation and letting me bouncing ideas around, and all the folks who
|
|
kindly shared their time for feedback and testing</li>
|
|
<li>Roll20 for helping keep my friends and me sane during the pandemic</li>
|
|
<li>Finally: THANK YOU for using this (and for your patience while bugs are worked out)</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sheet-section sheet-section-fields">
|
|
<input type="hidden" name="attr_charname_output" value="{{charname=@{character_name}}}">
|
|
<input type="hidden" name="attr_d20" value="1d20">
|
|
<input type="hidden" name="attr_xp_nextlevel" value="10">
|
|
<input type="hidden" name="attr_level" value="1">
|
|
<input type="hidden" name="attr_initiative_bonus" value="0">
|
|
<input type="hidden" name="attr_strength_mod" value="0">
|
|
<input type="hidden" name="attr_dexterity_mod" value="0">
|
|
<input type="hidden" name="attr_constitution_mod" value="0">
|
|
<input type="hidden" name="attr_intelligence_mod" value="0">
|
|
<input type="hidden" name="attr_wisdom_mod" value="0">
|
|
<input type="hidden" name="attr_charisma_mod" value="0">
|
|
<input type="hidden" name="attr_initiative_style" value="@{d20}">
|
|
<input type="hidden" name="attr_wtype" value="@{whispertoggle}">
|
|
<input type="hidden" name="attr_rtype" value="@{advantagetoggle}">
|
|
<input type="hidden" name="attr_init_tiebreaker" value="0">
|
|
</div>
|
|
<div class="sheet-section sheet-section-templates">
|
|
<div class="sheet-rolltemplates">
|
|
<rolltemplate class="sheet-rolltemplate-atk">
|
|
<!-- template for attacks and spell checks -->
|
|
<div class="sheet-container">
|
|
<div class="char-name">
|
|
<span>{{charname}}</span>
|
|
</div>
|
|
<div class="result">
|
|
{{#normal}}
|
|
<div class="solo">
|
|
<span>{{r1}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/normal}}
|
|
{{#advantage}}
|
|
{{#rollLess() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r2}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r2}}
|
|
{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{/rollGreater() r1 r2}}
|
|
{{/advantage}}
|
|
{{#disadvantage}}
|
|
{{#rollLess() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasFumble() r2}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r2}}
|
|
{{/rollGreater() r1 r2}}
|
|
{{/disadvantage}}
|
|
</div>
|
|
<div class="label">
|
|
<span>⚔️</span>
|
|
{{#normal}}
|
|
{{#rollWasCrit() r1}}<span>{{rnamec}}{{#innate}}<span>, {{innate}}</span>{{/innate}}
|
|
<span>({{mod}})</span></span>{{/rollWasCrit() r1}}
|
|
{{#^rollWasCrit() r1}}<span>{{rname}}{{#innate}}<span>, {{innate}}</span>{{/innate}}
|
|
<span>({{mod}})</span></span>{{/^rollWasCrit() r1}}
|
|
{{/normal}}
|
|
{{#always}}
|
|
{{#rollLess() r1 r2}}{{#rollWasCrit() r2}}<span>{{rnamec}}{{#innate}}<span>,
|
|
{{innate}}</span>{{/innate}} <span>({{mod}})</span></span>{{/rollWasCrit() r2}}{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}}<span>{{rnamec}}{{#innate}}<span>,
|
|
{{innate}}</span>{{/innate}} <span>({{mod}})</span></span>{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}{{#rollWasCrit() r1}}<span>{{rnamec}}{{#innate}}<span>,
|
|
{{innate}}</span>{{/innate}} <span>({{mod}})</span></span>{{/rollWasCrit() r1}}{{/rollGreater() r1 r2}}
|
|
{{#^rollWasCrit() r1}}{{#^rollWasCrit() r2}}<span>{{rname}}{{#innate}}<span>,
|
|
{{innate}}</span>{{/innate}} <span>({{mod}})</span></span>{{/^rollWasCrit() r2}}{{/^rollWasCrit() r1}}
|
|
{{/always}}
|
|
{{#advantage}}
|
|
{{#rollLess() r1 r2}}{{#rollWasCrit() r2}}<span>{{rnamec}}{{#innate}}<span>,
|
|
{{innate}}</span>{{/innate}} <span>({{mod}})</span></span>{{/rollWasCrit() r2}}{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}}<span>{{rnamec}}{{#innate}}<span>,
|
|
{{innate}}</span>{{/innate}} <span>({{mod}})</span></span>{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}{{#rollWasCrit() r1}}<span>{{rnamec}}{{#innate}}<span>,
|
|
{{innate}}</span>{{/innate}} <span>({{mod}})</span></span>{{/rollWasCrit() r1}}{{/rollGreater() r1 r2}}
|
|
{{#^rollWasCrit() r1}}{{#^rollWasCrit() r2}}<span>{{rname}}{{#innate}}<span>,
|
|
{{innate}}</span>{{/innate}} <span>({{mod}})</span></span>{{/^rollWasCrit() r2}}{{/^rollWasCrit() r1}}
|
|
{{/advantage}}
|
|
{{#disadvantage}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}}<span>{{rnamec}}{{#innate}}<span>,
|
|
{{innate}}</span>{{/innate}} <span>({{mod}})</span></span>{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{#rollTotal() r1 r2}}{{#^rollWasCrit() r1}}<span>{{rname}}{{#innate}}<span>,
|
|
{{innate}}</span>{{/innate}} <span>({{mod}})</span></span>{{/^rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{#rollLess() r1 r2}}<span>{{rname}}{{#innate}}<span>, {{innate}}</span>{{/innate}}
|
|
<span>({{mod}})</span></span>{{/rollLess() r1 r2}}
|
|
{{#rollGreater() r1 r2}}<span>{{rname}}{{#innate}}<span>, {{innate}}</span>{{/innate}}
|
|
<span>({{mod}})</span></span>{{/rollGreater() r1 r2}}
|
|
{{/disadvantage}}
|
|
</div>
|
|
{{#castdc}}
|
|
<div class="info">
|
|
<span>Cast DC: {{castdc}}</span>
|
|
</div>
|
|
{{/castdc}}
|
|
{{#range}}
|
|
<div class="info">
|
|
<span>Range: {{range}}</span>
|
|
</div>
|
|
{{/range}}
|
|
</div>
|
|
</rolltemplate>
|
|
<rolltemplate class="sheet-rolltemplate-dmg">
|
|
<!-- template for rolling damage -->
|
|
<div class="sheet-container">
|
|
<div class="char-name">
|
|
<span>{{charname}}: {{rname}}</span>
|
|
</div>
|
|
<div class="damagetemplate">
|
|
<div class="result">
|
|
{{#dmg1flag}}
|
|
{{^dmg2flag}}
|
|
<div class="solo">
|
|
<span class="damage">{{dmg1}}{{#crit}} + {{crit1}}{{/crit}}</span>
|
|
</div>
|
|
<div>Damage</div>
|
|
{{/dmg2flag}}
|
|
{{/dmg1flag}}
|
|
{{#dmg2flag}}
|
|
{{^dmg1flag}}
|
|
<div class="solo">
|
|
<span class="damage">{{dmg2}}{{#crit}} + {{crit2}}{{/crit}}</span>
|
|
</div>
|
|
<div>Damage</div>
|
|
{{/dmg1flag}}
|
|
{{/dmg2flag}}
|
|
{{#dmg1flag}}
|
|
{{#dmg2flag}}
|
|
<div class="adv">
|
|
<span class="damage">{{dmg1}}{{#crit}} + {{crit1}}{{/crit}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="damage">{{dmg2}}{{#crit}} + {{crit2}}{{/crit}}</span>
|
|
</div>
|
|
<div>Damage</div>
|
|
{{/dmg2flag}}
|
|
{{/dmg1flag}}
|
|
</div>
|
|
{{#duration}}
|
|
<div class="info">
|
|
<span>Duration: {{duration}}</span>
|
|
</div>
|
|
{{/duration}}
|
|
{{#desc}}
|
|
<div class="info">
|
|
<span>
|
|
<span class="top"></span><span class="middle">{{desc}}</span><span class="bottom"></span>
|
|
</span>
|
|
</div>
|
|
{{/desc}}
|
|
</div>
|
|
</div>
|
|
</rolltemplate>
|
|
<rolltemplate class="sheet-rolltemplate-atkdmg">
|
|
<!-- template for combined attack and damage rolls -->
|
|
<div class="sheet-container">
|
|
<div class="char-name">
|
|
<span>{{charname}}</span>: <span>{{rname}} <span>({{mod}})</span></span>
|
|
</div>
|
|
{{#attack}}
|
|
<div class="result">
|
|
{{#normal}}
|
|
<div class="solo">
|
|
<span>{{r1}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/normal}}
|
|
{{#advantage}}
|
|
{{#rollLess() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r2}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r2}}
|
|
{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{/rollGreater() r1 r2}}
|
|
{{/advantage}}
|
|
{{#disadvantage}}
|
|
{{#rollLess() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasFumble() r2}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r2}}
|
|
{{/rollGreater() r1 r2}}
|
|
{{/disadvantage}}
|
|
</div>
|
|
{{#castdc}}
|
|
<div class="info">
|
|
<span>Cast DC: {{castdc}}</span>
|
|
</div>
|
|
{{/castdc}}
|
|
{{#range}}
|
|
<div class="info">
|
|
<span>Range: {{range}}</span>
|
|
</div>
|
|
{{/range}}
|
|
{{/attack}}
|
|
{{#duration}}
|
|
<div class="info">
|
|
<span>Duration: {{duration}}</span>
|
|
</div>
|
|
{{/duration}}
|
|
{{#desc}}
|
|
<div class="info">
|
|
<span>
|
|
<span class="top"></span><span class="middle">{{desc}}</span><span class="bottom"></span>
|
|
</span>
|
|
</div>
|
|
{{/desc}}
|
|
{{#damage}}
|
|
<div class="result">
|
|
{{#dmg1flag}}
|
|
{{^dmg2flag}}
|
|
<div class="solo">
|
|
<span class="damage">
|
|
{{dmg1}}
|
|
{{#attack}}
|
|
{{#normal}}
|
|
{{#rollWasCrit() r1}} + {{crit1}}{{/rollWasCrit() r1}}
|
|
{{/normal}}
|
|
{{#advantage}}
|
|
{{#rollLess() r1 r2}}{{#rollWasCrit() r2}} + {{crit1}}{{/rollWasCrit() r2}}{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}} + {{crit1}}{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}{{#rollWasCrit() r1}} + {{crit1}}{{/rollWasCrit() r1}}{{/rollGreater() r1 r2}}
|
|
{{/advantage}}
|
|
{{#disadvantage}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}} + {{crit1}}{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{/disadvantage}}
|
|
{{/attack}}
|
|
</span>
|
|
</div>
|
|
<div>Damage</div>
|
|
{{/dmg2flag}}
|
|
{{/dmg1flag}}
|
|
{{#dmg2flag}}
|
|
{{^dmg1flag}}
|
|
<div class="solo">
|
|
<span class="damage">
|
|
{{dmg2}}
|
|
{{#attack}}
|
|
{{#normal}}
|
|
{{#rollWasCrit() r1}} + {{crit2}}{{/rollWasCrit() r1}}
|
|
{{/normal}}
|
|
{{#advantage}}
|
|
{{#rollLess() r1 r2}}{{#rollWasCrit() r2}} + {{crit2}}{{/rollWasCrit() r2}}{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}} + {{crit2}}{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}{{#rollWasCrit() r1}} + {{crit2}}{{/rollWasCrit() r1}}{{/rollGreater() r1 r2}}
|
|
{{/advantage}}
|
|
{{#disadvantage}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}} + {{crit2}}{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{/disadvantage}}
|
|
{{/attack}}
|
|
</span>
|
|
</div>
|
|
<div>Damage</div>
|
|
{{/dmg1flag}}
|
|
{{/dmg2flag}}
|
|
{{#dmg1flag}}
|
|
{{#dmg2flag}}
|
|
<div class="adv">
|
|
<span class="damage">
|
|
{{dmg1}}
|
|
{{#attack}}
|
|
{{#normal}}
|
|
{{#rollWasCrit() r1}} + {{crit1}}{{/rollWasCrit() r1}}
|
|
{{/normal}}
|
|
{{#advantage}}
|
|
{{#rollLess() r1 r2}}{{#rollWasCrit() r2}} + {{crit1}}{{/rollWasCrit() r2}}{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}} + {{crit1}}{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}{{#rollWasCrit() r1}} + {{crit1}}{{/rollWasCrit() r1}}{{/rollGreater() r1 r2}}
|
|
{{/advantage}}
|
|
{{#disadvantage}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}} + {{crit1}}{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{/disadvantage}}
|
|
{{/attack}}
|
|
</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="damage">
|
|
{{dmg2}}
|
|
{{#attack}}
|
|
{{#normal}}
|
|
{{#rollWasCrit() r1}} + {{crit2}}{{/rollWasCrit() r1}}
|
|
{{/normal}}
|
|
{{#advantage}}
|
|
{{#rollLess() r1 r2}}{{#rollWasCrit() r2}} + {{crit2}}{{/rollWasCrit() r2}}{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}} + {{crit2}}{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}{{#rollWasCrit() r1}} + {{crit2}}{{/rollWasCrit() r1}}{{/rollGreater() r1 r2}}
|
|
{{/advantage}}
|
|
{{#disadvantage}}
|
|
{{#rollTotal() r1 r2}}{{#rollWasCrit() r1}} + {{crit2}}{{/rollWasCrit() r1}}{{/rollTotal() r1 r2}}
|
|
{{/disadvantage}}
|
|
{{/attack}}
|
|
</span>
|
|
</div>
|
|
<div>Damage</div>
|
|
{{/dmg2flag}}
|
|
{{/dmg1flag}}
|
|
</div>
|
|
{{/damage}}
|
|
</div>
|
|
</rolltemplate>
|
|
<rolltemplate class="sheet-rolltemplate-simple">
|
|
<!-- template for stat checks and initiative -->
|
|
<div class="sheet-container">
|
|
<div class="char-name">
|
|
<span>{{charname}}</span>
|
|
</div>
|
|
<div class="roll-name">
|
|
<span>{{rname}}{{#mod}} <span>({{mod}})</span>{{/mod}}</span>
|
|
</div>
|
|
<div class="result">
|
|
{{#isinit}}
|
|
<div>
|
|
<span class="roll-initiative">{{r1}}</span>
|
|
</div>
|
|
{{/isinit}}
|
|
{{#normal}}
|
|
<div class="solo">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/normal}}
|
|
{{#advantage}}
|
|
{{#rollLess() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r2}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r2}}
|
|
{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{/rollGreater() r1 r2}}
|
|
{{/advantage}}
|
|
{{#disadvantage}}
|
|
{{#rollLess() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-used">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/rollLess() r1 r2}}
|
|
{{#rollTotal() r1 r2}}
|
|
<div class="adv">
|
|
<span>{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasCrit() r1}}<div class="crit-success">Critical Success!</div>{{/rollWasCrit() r1}}
|
|
{{#rollWasFumble() r1}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r1}}
|
|
{{/rollTotal() r1 r2}}
|
|
{{#rollGreater() r1 r2}}
|
|
<div class="adv">
|
|
<span class="roll-unused">{{r1}}</span>
|
|
</div>
|
|
<div class="advspacer"></div>
|
|
<div class="adv">
|
|
<span class="roll-used">{{r2}}</span>
|
|
</div>
|
|
{{#rollWasFumble() r2}}<div class="crit-fail">Critical Failure!</div>{{/rollWasFumble() r2}}
|
|
{{/rollGreater() r1 r2}}
|
|
{{/disadvantage}}
|
|
</div>
|
|
</div>
|
|
</rolltemplate>
|
|
<rolltemplate class="sheet-rolltemplate-traits">
|
|
<!-- template for sharing traits and features -->
|
|
<div class="sheet-container">
|
|
<div class="char-name">
|
|
{{charname}}
|
|
</div>
|
|
<div class="trait-name">
|
|
{{name}}
|
|
</div>
|
|
{{#description}}
|
|
<div class="trait-desc">
|
|
{{description}}
|
|
</div>
|
|
{{/description}}
|
|
</div>
|
|
</rolltemplate>
|
|
</div>
|
|
</div>
|
|
<script type="text/worker">
|
|
//////////
|
|
/// required text for Shadowdark RPG Third-Party License Version 1.1:
|
|
/// "Roll20 Shadowdark RPG Character Sheet is an independent product published under the
|
|
/// Shadowdark RPG Third-Party License and is not affiliated with The Arcane Library,
|
|
/// LLC. Shadowdark RPG © 2023 The Arcane Library, LLC."
|
|
//////////
|
|
|
|
const core_stats = ["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"];
|
|
|
|
// Min slots = 10; Slots = STR score (which can exceed 18) + Fighter can use CON mod
|
|
// Stats can go higher than 18 so it is possible a PC could exceed 20 slots. if so, use the treasure box for overflow.
|
|
const maxSheetSlots = 20;
|
|
const minPcSlots = 0; // per core rules, min PC slots is 10, but making it 0 in case some debuff/curse/etc happens
|
|
|
|
// SD core rules goes from levels 0-10 (class abilities start at level 1)
|
|
const minLevel = 0;
|
|
const maxLevel = 10;
|
|
const maxTotalXp = 450;
|
|
|
|
// decoder ring for parsing monster/NPC alignment codes
|
|
const alignments = {
|
|
"C": "Chaotic",
|
|
"L": "Lawful",
|
|
"N": "Neutral"
|
|
};
|
|
|
|
on("clicked:importmonster", function () {
|
|
// prompt user to replace stats
|
|
getAttrs(["sheetwipe_confirm_flag", "character_name", "json"], function (v) {
|
|
if (v.sheetwipe_confirm_flag !== "1" || !v.json?.length) {
|
|
console.log("You must accept the disclaimer and input some text before importing a monster.")
|
|
return;
|
|
}
|
|
try {
|
|
const monster = parseMonster(v.json);
|
|
const isNpc = true;
|
|
if (monster) {
|
|
import_sheet(monster, isNpc);
|
|
} else {
|
|
throw new Error("No data returned from parseMonster()");
|
|
}
|
|
}
|
|
catch (ex) {
|
|
console.log("Monster import failed.");
|
|
console.log(ex);
|
|
}
|
|
});
|
|
});
|
|
|
|
on("clicked:importjson", function () {
|
|
// prompt user to replace stats
|
|
getAttrs(["sheetwipe_confirm_flag", "character_name", "json"], function (v) {
|
|
if (v.sheetwipe_confirm_flag !== "1" || !v.json?.length) {
|
|
console.log("You must accept the disclaimer and input some JSON before importing a sheet.")
|
|
return;
|
|
}
|
|
try {
|
|
const importedSheetObj = JSON.parse(v.json);
|
|
const isNpc = importedSheetObj.class === undefined; // assumption: no char class = NPC (consistent with Shadowdarklings which only exports PC JSON, for now)
|
|
if (!importedSheetObj) return;
|
|
import_sheet(importedSheetObj, isNpc);
|
|
}
|
|
catch (ex) {
|
|
console.log("Sheet import failed.");
|
|
console.log(ex);
|
|
}
|
|
});
|
|
});
|
|
|
|
// convert string to title case
|
|
let titleCase = function (str) {
|
|
if (!str) return '';
|
|
return str.replace(/\w\S*/g, function (txt) {
|
|
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
|
});
|
|
}
|
|
|
|
// parse monster from copy-and-paste book format to shadowdarklings-style format
|
|
let parseMonster = function (text) {
|
|
const entries = [];
|
|
let entry = [];
|
|
let lines = text.split(/\n/)
|
|
.map(o => o.trim())
|
|
.filter(o => o.length);
|
|
if (lines.length < 3) {
|
|
throw new Error(
|
|
`This doesn't look like valid monster text. Expected format is:\n` +
|
|
`NAME\n` +
|
|
`Description text.\n` +
|
|
`AC 1 (armor type), HP 1, ATK 1 hammer +1 (1d4), MV near,\n` +
|
|
`S +1, D +0, C +0, I -1, W +0, Ch -1, AL C, LV 1\n` +
|
|
`Action 1. Action 1 description.\n` +
|
|
`Action N. Action N description.`
|
|
);
|
|
}
|
|
|
|
// Assume first line is the monster name
|
|
let acIndex = 0;
|
|
let monster = {
|
|
name: titleCase(lines[0]),
|
|
description: "",
|
|
actions: [],
|
|
stats: {}
|
|
};
|
|
|
|
for (let l = 0; l < lines.length; ++l) {
|
|
let line = lines[l];
|
|
// Assume AC is first stat. Everything between name and AC is Description text.
|
|
// Hopefully there is no monster with "AC " in the description... shrug..
|
|
if (monster.description.length && /^(A[Cc]\s+\d+|AC$)/.test(line)) {
|
|
acIndex = l;
|
|
break; // found start of stat block; exit loop
|
|
} else {
|
|
if (monster.description.length) monster.description += " ";
|
|
monster.description += line;
|
|
}
|
|
}
|
|
|
|
// push monster only if found the stats block
|
|
if (acIndex === 0) {
|
|
console.log(`Unable to parse stats for ${monster.name}`);
|
|
return monster;
|
|
}
|
|
|
|
// stats come before actions - first stat is "AC" (armor class) and final stat is "LV" (level)
|
|
// attacks are included in the stats block using "ATK"
|
|
const statsAndActionsLines = lines.slice(acIndex);
|
|
|
|
// find beginning of actions text
|
|
const actionsIndex = statsAndActionsLines.findIndex(o => /(^\d+|[Ll][Vv]\s(\d+|\*)(\/\d+)?)$/.test(o)) + 1;
|
|
const actionsLines = statsAndActionsLines.slice(actionsIndex);
|
|
try {
|
|
monster.actions = parseActions(actionsLines);
|
|
} catch (ex) {
|
|
console.log(`Failed to parse actions for ${monster.name}`);
|
|
console.log(ex);
|
|
}
|
|
|
|
// slice out and parse stats from between monster description and actions
|
|
const statsLines = statsAndActionsLines.slice(0, actionsIndex).map(o => o.trim()).join(" ").split(/,/);
|
|
for (let i = 0; i < statsLines.length; i++) {
|
|
let statsLine = statsLines[i].trim(); // Trim leading and trailing whitespace for copy-paste issues
|
|
if (!statsLine?.length) continue; // skip empty lines
|
|
try {
|
|
if (!monster.armorClass && statsLine.startsWith('AC ')) {
|
|
const [, AC, armor] = statsLine.match(/AC (\d+)(?: \(([\w\s]+)\))?/);
|
|
monster.armorClass = +AC;
|
|
monster.armor = armor || "";
|
|
} else if (!monster.maxHitPoints && /^[Hh][Pp]\s(\*|\d+)(\/\d+)?$/.test(statsLine)) {
|
|
// if there is a rare case of multi-value like 4/5 grab the first number (ex: elementals don't have separate lesser/greater stats in the book)
|
|
const hp = /^[Hh][Pp]\s(?<hp>\*|\d+)(\/\d+)?$/.exec(statsLine).groups?.hp || "";
|
|
monster.maxHitPoints = parseInt(hp, 10) || (monster.name === "Hydra" ? 55 : 0); // hydra heads special case (5 heads)
|
|
} else if (!monster.attacks && statsLine.startsWith('ATK ')) {
|
|
try {
|
|
// hacky fix for Azer (and other stat blocks that have comma in the ATK section)
|
|
if (/^\s+[^A-Z]/.test(statsLines[i + 1])) {
|
|
statsLine += ' + ' + statsLines[i + 1]; // restore comma
|
|
}
|
|
const [, attackText] = statsLine.match(/ATK (.+)/);
|
|
const attackStrings = attackText.split(/\bor\b|\band\b/).map(s => s.trim());
|
|
const attacks = attackStrings.map(s =>
|
|
/^(?<qty>\d+(d\d+)?)\s*(?<name>(\+\d+)?[\w\s]*)\b(\s*)?(\((?<rng>([\w\s/]+))\)\s*)?(?<mod>[\+|\-]\d+)?(\s*)?[\(]?(?<dmg>(\d+[Dd]\d+|\d+)(\s\+\s\d+)?)?(\/\d+[Dd]\d+)?(\s*)?[\+,]?(\s+)?(?<effect>[^\)][\w\s\+]*)?\)?/g
|
|
.exec(s)?.groups).filter(s => s !== undefined);
|
|
monster.attackText = attackText;
|
|
monster.attacks = attacks.map(attack => {
|
|
return {
|
|
name: titleCase(attack.name || ""),
|
|
qty: parseInt(attack.qty, 10),
|
|
range: attack.rng || "",
|
|
damage: attack.dmg || "",
|
|
mod: parseInt(attack.mod, 10) || undefined,
|
|
effect: attack.effect || ""
|
|
};
|
|
});
|
|
} catch (ex) {
|
|
console.log(`Failed to parse attacks for ${monster.name}`);
|
|
console.log(ex);
|
|
}
|
|
} else if (!monster.movement && /^[Mm][Vv]\s[\(\s\w/]*[\w\)]$/.test(statsLine)) {
|
|
// hacky fix for Tarrasque (and other stat blocks that have comma in the MV section)
|
|
if (/^\s+[^A-Z]/.test(statsLines[i + 1])) {
|
|
statsLine += ', ' + statsLines[i + 1]; // restore comma
|
|
}
|
|
monster.movement = statsLine.substring(3);
|
|
} else if (!monster.stats.str_mod && /^[Ss]\s[\+\-]+[\d]+$/.test(statsLine)) {
|
|
monster.stats.str_mod = parseInt(statsLine.substring(2), 10) || 0;
|
|
monster.stats.STR = Math.max(1, 10 + 2 * monster.stats.str_mod); // derive core stat
|
|
} else if (!monster.stats.dex_mod && /^[Dd]\s[\+\-]+[\d]+$/.test(statsLine)) {
|
|
monster.stats.dex_mod = parseInt(statsLine.substring(2), 10) || 0;
|
|
monster.stats.DEX = Math.max(1, 10 + 2 * monster.stats.dex_mod); // derive core stat
|
|
} else if (!monster.stats.con_mod && /^[Cc]\s[\+\-]+[\d]+$/.test(statsLine)) {
|
|
monster.stats.con_mod = parseInt(statsLine.substring(2), 10) || 0;
|
|
monster.stats.CON = Math.max(1, 10 + 2 * monster.stats.con_mod); // derive core stat
|
|
} else if (!monster.stats.int_mod && /^[Ii]\s[\+\-]+[\d]+$/.test(statsLine)) {
|
|
monster.stats.int_mod = parseInt(statsLine.substring(2), 10) || 0;
|
|
monster.stats.INT = Math.max(1, 10 + 2 * monster.stats.int_mod); // derive core stat
|
|
} else if (!monster.stats.wis_mod && /^[Ww]\s[\+\-]+[\d]+$/.test(statsLine)) {
|
|
monster.stats.wis_mod = parseInt(statsLine.substring(2), 10) || 0;
|
|
monster.stats.WIS = Math.max(1, 10 + 2 * monster.stats.wis_mod); // derive core stat
|
|
} else if (!monster.stats.cha_mod && /^[Cc][Hh]\s[\+\-]+[\d]+$/.test(statsLine)) {
|
|
monster.stats.cha_mod = parseInt(statsLine.substring(3), 10) || 0;
|
|
monster.stats.CHA = Math.max(1, 10 + 2 * monster.stats.cha_mod); // derive core stat
|
|
} else if (!monster.alignment && /^[Aa][Ll]\s+[\(\w\s]*[\w\)]$/.test(statsLine)) {
|
|
monster.alignment = alignments[statsLine.substring(3)];
|
|
} else if (!monster.level && /^[Ll][Vv]\s(\*|\d+)(\/\d+)?$/.test(statsLine)) {
|
|
// if there is a rare case of multi-value like 4/5 grab the first number (ex: elementals don't have separate lesser/greater stats in the book)
|
|
const levelText = /^[Ll][Vv]\s(?<level>\*|\d+)(\/\d+)?$/.exec(statsLine).groups?.level || "";
|
|
monster.level = parseInt(levelText, 10) || (monster.name === "Hydra" ? 10 : 0); // hydra heads special case (5 heads)
|
|
break; // done getting stats - get attack text next loop
|
|
}
|
|
}
|
|
catch (ex) {
|
|
console.log(ex);
|
|
console.log(monster);
|
|
}
|
|
}
|
|
return monster;
|
|
}
|
|
|
|
let parseActions = function (inputArray) {
|
|
const result = [];
|
|
|
|
for (let i = 0; i < inputArray.length; i++) {
|
|
const actionObject = {
|
|
name: '',
|
|
description: ''
|
|
};
|
|
|
|
// action name
|
|
const actionNameRegex = /^[A-Z][^A-Z][^.]*/;
|
|
const actionNameMatch = inputArray[i].match(actionNameRegex);
|
|
if (actionNameMatch) {
|
|
actionObject.name = actionNameMatch[0].trim();
|
|
}
|
|
|
|
// description
|
|
const descriptionRegex = /\..*?(\.|$)/;
|
|
const descriptionMatch = inputArray[i].match(descriptionRegex);
|
|
if (descriptionMatch) {
|
|
actionObject.description = inputArray[i].substring(descriptionMatch.index + 1).trim();
|
|
|
|
// concatenate subsequent lines for description
|
|
let j = i + 1;
|
|
while (j < inputArray.length && !actionNameRegex.test(inputArray[j])) {
|
|
actionObject.description += ' ' + inputArray[j].trim();
|
|
j++;
|
|
}
|
|
|
|
i = j - 1; // skip processed lines
|
|
}
|
|
|
|
// sanity check - add only if both name and description are not empty
|
|
if (actionObject.name !== '' && actionObject.description !== '') {
|
|
result.push(actionObject);
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
let import_sheet = function (sheet, isNpc) {
|
|
console.log("Importing custom sheet:\n" + JSON.stringify(sheet));
|
|
|
|
// adjust slots based on STR score (or min=10) if needed
|
|
if (sheet.gearSlotsTotal === undefined) {
|
|
sheet.gearSlotsTotal = sheet.stats?.STR ? Math.max(sheet.stats?.STR, 10) : 10;
|
|
}
|
|
|
|
// set core stats, attributes, and non-repeater items. don't set name because throws exception in popout window mode
|
|
const statModMax = !isNpc ? 4 : 99;
|
|
let update = {
|
|
npc: isNpc ? "1" : "0",
|
|
sheetwipe_confirm_flag: "0", // clear flag to prevent accidental wipes
|
|
xp: sheet.XP || 0,
|
|
base_level: sheet.level || 0,
|
|
lifetime_xp: getTotalXpForLevel(sheet.level || 0) + (sheet.XP || 0),
|
|
xp_nextlevel: sheet.level < 1 || sheet.level >= 10 ? 0 : sheet.level * 10,
|
|
movement: sheet.movement || "",
|
|
description: sheet.description || "",
|
|
armor: sheet.armor || "",
|
|
class: sheet.class || "",
|
|
title: sheet.title || "",
|
|
ancestry: sheet.ancestry || "",
|
|
background: sheet.background || "",
|
|
alignment: sheet.alignment || "",
|
|
deity: sheet.deity || "",
|
|
hp: sheet.maxHitPoints || 0,
|
|
hp_max: sheet.maxHitPoints || 0,
|
|
ac: sheet.armorClass || 10,
|
|
strength: sheet.stats?.STR || 10,
|
|
dexterity: sheet.stats?.DEX || 10,
|
|
constitution: sheet.stats?.CON || 10,
|
|
intelligence: sheet.stats?.INT || 10,
|
|
wisdom: sheet.stats?.WIS || 10,
|
|
charisma: sheet.stats?.CHA || 10,
|
|
strength_base: sheet.stats?.STR || 10,
|
|
dexterity_base: sheet.stats?.DEX || 10,
|
|
constitution_base: sheet.stats?.CON || 10,
|
|
intelligence_base: sheet.stats?.INT || 10,
|
|
wisdom_base: sheet.stats?.WIS || 10,
|
|
charisma_base: sheet.stats?.CHA || 10,
|
|
strength_mod: sheet.stats?.str_mod !== undefined ? sheet.stats.str_mod : Math.min(statModMax, Math.floor(((sheet.stats?.STR || 10) - 10) / 2)),
|
|
dexterity_mod: sheet.stats?.dex_mod !== undefined ? sheet.stats.dex_mod : Math.min(statModMax, Math.floor(((sheet.stats?.DEX || 10) - 10) / 2)),
|
|
constitution_mod: sheet.stats?.con_mod !== undefined ? sheet.stats.con_mod : Math.min(statModMax, Math.floor(((sheet.stats?.CON || 10) - 10) / 2)),
|
|
intelligence_mod: sheet.stats?.int_mod !== undefined ? sheet.stats.int_mod : Math.min(statModMax, Math.floor(((sheet.stats?.INT || 10) - 10) / 2)),
|
|
wisdom_mod: sheet.stats?.wis_mod !== undefined ? sheet.stats.wis_mod : Math.min(statModMax, Math.floor(((sheet.stats?.WIS || 10) - 10) / 2)),
|
|
charisma_mod: sheet.stats?.cha_mod !== undefined ? sheet.stats.cha_mod : Math.min(statModMax, Math.floor(((sheet.stats?.CHA || 10) - 10) / 2)),
|
|
slots_total: sheet.gearSlotsTotal,
|
|
slots: sheet.gearSlotsUsed || 0,
|
|
gp: sheet.gold || 0,
|
|
sp: sheet.silver || 0,
|
|
cp: sheet.copper || 0
|
|
};
|
|
|
|
// add imported gear
|
|
let slotGear = [];
|
|
let noSlotGear = [];
|
|
if (sheet.gear?.length) {
|
|
try {
|
|
slotGear = sheet.gear
|
|
.filter(o => o.slots > 0 && o.quantity > 0)
|
|
.flatMap((o) => {
|
|
if (o.slots > 1 && o.quantity <= o.slots) {
|
|
// fill by slots (will include "big" items taking > 1 slot)
|
|
return new Array(o.slots).fill(o.name);
|
|
}
|
|
else if ((o.totalUnits && o.totalUnits > 1) || o.quantity > o.slots) {
|
|
// account for stackables like ammo where qty > slots
|
|
// if only 1 piece of ammo, no worries. it is handled below.
|
|
// first version: if more than 1 slot, then divide stacks ~equally. don't care about hardcoding arbitrary stack size rules.
|
|
let totalUnits = o.totalUnits || o.quantity;
|
|
const stackSize = Math.floor(totalUnits / o.slots);
|
|
const stacks = new Array(o.slots);
|
|
while (totalUnits > 0) {
|
|
stackQty = totalUnits -= Math.min(totalUnits, stackSize);
|
|
stacks.push(o.name + " (" + o.totalUnits + ")");
|
|
}
|
|
return stacks;
|
|
}
|
|
// fall back to fill by qty
|
|
return new Array(o.quantity).fill(o.name);
|
|
});
|
|
noSlotGear = sheet.gear
|
|
.filter(o => o.slots === 0)
|
|
.map(o => o.name + " (" + o.quantity + ")");
|
|
|
|
// shove magic items and treasures in no slot gear with details, and add slot it used.
|
|
if (sheet.treasures?.length) {
|
|
noSlotGear = noSlotGear.concat(sheet.treasures.map(o => {
|
|
const sb = [o.name];
|
|
if (o.desc?.length) sb.push(": " + o.desc);
|
|
if (o.cost && o.currency) sb.push("\n" + o.cost + " " + o.currency);
|
|
if (o.slots) {
|
|
sb.push(" (" + o.slots + " slot" + (o.slots > 1 ? "s" : "") + ")");
|
|
slotGear = slotGear.concat(new Array(o.slots).fill(o.name));
|
|
}
|
|
return sb.join("");
|
|
}));
|
|
}
|
|
if (sheet.magicItems?.length) {
|
|
noSlotGear = noSlotGear.concat(sheet.magicItems.map(o => {
|
|
const sb = [o.name];
|
|
if (o.features?.length) sb.push(": " + o.features);
|
|
|
|
// as of 15 Jan 2024, Shadowdarklings has a bug where magic items don't include slot count in JSON. assume 1 slot until fixed. probably matters only for armor. (reported bug via discord)
|
|
let slots = o.slots || 1;
|
|
sb.push(" (" + slots + " slot" + (slots > 1 ? "s" : "") + ")");
|
|
slotGear = slotGear.concat(new Array(slots).fill(o.name));
|
|
|
|
if (o.bonus) sb.push("\nBonus: " + o.bonus);
|
|
if (o.bonusNote?.length) sb.push("\n" + o.bonusNote);
|
|
if (o.attackNote?.length) sb.push("\n" + o.attackNote);
|
|
if (o.benefits?.length) sb.push("\nBenefits: " + o.benefits);
|
|
if (o.curses?.length) sb.push("\nCurses: " + o.curses);
|
|
if (o.hasPersonality) sb.push("\nPersonality:");
|
|
if (o.personalityVirtue?.length) sb.push("\nVirtues: " + o.personalityVirtue);
|
|
if (o.personalityFlaws?.length) sb.push("\nFlaws: " + o.personalityFlaws);
|
|
if (o.personalityTraits?.length) sb.push("\nTraits: " + o.personalityTraits);
|
|
return sb.join("");
|
|
}));
|
|
}
|
|
update["gearnoslots"] = noSlotGear.join("\n\n");
|
|
}
|
|
catch (ex) {
|
|
console.log("Sorry, but there was a problem parsing gear from this custom sheet. Can you clean it up or make it simpler, and try again?");
|
|
}
|
|
}
|
|
for (let i = 0; i < maxSheetSlots; ++i) {
|
|
update["gearslot" + (i + 1)] = slotGear[i] || "";
|
|
}
|
|
|
|
// update derived stats
|
|
update["initiative_bonus"] = +update["dexterity_mod"] || 0;
|
|
|
|
// add attacks
|
|
// first, separate out "SPELLS" entry and add it as a feature instead.
|
|
const spellCastingFeatures = sheet.attacks?.filter(o=>/^SPELLS?:/i.test(o));
|
|
if (spellCastingFeatures?.length) {
|
|
spellCastingFeatures.forEach(o=>{
|
|
create_trait_from_feature({
|
|
name: "Spellcasting",
|
|
description: o.replace(/^SPELLS?:\s*/i,"")
|
|
});
|
|
});
|
|
}
|
|
|
|
if(!isNpc) {
|
|
// now, loop through remaining attacks and add them, use try-catch to safely parse the attack into its properties like damage
|
|
// ex "STAFF+1 (STAFF +1): +1, 1d4+1 (2H), Magic",
|
|
// Name = "STAFF+1" Magic Bonus (gets applied to attacks AND damage) = 1; damage = "1d4"; description: "Magic STAFF+1 (2H)"
|
|
const normalAttacks = sheet.attacks?.filter(o=>!/^SPELLS?:/i.test(o));
|
|
if (normalAttacks?.length) {
|
|
normalAttacks.forEach(o=>{
|
|
try {
|
|
const attackRegex = /^(?<name>[\w\s\+\-]+)(\s*\((?<altName>[\w\s\+\-]+)\))?:\s*(?<toHit>[\+\-]?\d+),\s*(?<damage>\d+[dD]\d+)(?<magBonus>[\+\-]\d+)?(\s*\((?<hands>[\w\s\+\-]+)\))?(,\s*(?<effect>.*))?$/;
|
|
const match = attackRegex.exec(o);
|
|
if (match?.groups && match.groups.damage && match.groups.damage && match.groups.name?.length) {
|
|
const attackAction = {
|
|
name: match.groups.name + (match.hands?.length ? ` (${match.hands})` : '' ),
|
|
baseAttr: "@{strength_mod}", // default weapons to STR
|
|
//range: match.groups.hands || "",
|
|
damage: match.groups.damage,
|
|
magBonus: match.groups.magBonus || "",
|
|
effect: (match.groups.altName ? ("(" + match.groups.altName + ") ") : "") + (match.groups.effect || "")
|
|
};
|
|
create_attack_from_action(attackAction);
|
|
} else {
|
|
throw new Error("Regex did not match");
|
|
}
|
|
}
|
|
catch (ex) {
|
|
console.log(`Failed to parse attack ${o}. Adding name only.`);
|
|
create_attack_from_action({
|
|
name: o,
|
|
description: "",
|
|
baseAttr: "@{strength_mod}", // default weapons to STR
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
// add languages
|
|
if (sheet.languages?.length) {
|
|
create_trait_from_feature({ name: "Languages Known", description: sheet.languages });
|
|
}
|
|
}
|
|
catch (ex) {
|
|
console.log(`Failed to import languages\n${ex}`);
|
|
}
|
|
|
|
// add spells
|
|
if (sheet.spellsKnown?.length && sheet.spellsKnown !== "None") {
|
|
|
|
try {
|
|
// create a "spell book" feature to list all known spells
|
|
create_trait_from_feature({ name: "Spells Known", description: sheet.spellsKnown });
|
|
}
|
|
catch (ex) {
|
|
console.log(`Failed to import spells list\n${ex}`);
|
|
}
|
|
|
|
// assume comma delimited list of spells and that spell names do not contain comma
|
|
const spells = sheet.spellsKnown.split(",").map(o => o.trim()).filter(o => o.length);
|
|
|
|
// for each spell, add an attack action if we can find the spell in the "compendium" which is an approved for third party content list of spells
|
|
spells.forEach(spellName => {
|
|
try {
|
|
let spell = compendium.spells.find(o => o.name === spellName);
|
|
if (!(spell?.name?.length)) {
|
|
// spell is not part of core rules and probably protected by License - add name only
|
|
spell = {
|
|
name: spellName,
|
|
description: `[imported spell ${spellName} was not found in core rules]`,
|
|
dc: "11",
|
|
range: "?",
|
|
damage: undefined,
|
|
tier: "?",
|
|
duration: "?"
|
|
}
|
|
}
|
|
|
|
const spellAction = {
|
|
name: spell.name,
|
|
baseAttr: `@{${(classCastingStat[sheet.class?.toLowerCase()] || 'charisma')}_mod}`, // ex: "@{intelligence_mod}"
|
|
duration: spell.duration,
|
|
dc: spell.dc,
|
|
range: spell.range,
|
|
damage: spell.healing?.length ? spell.healing : spell.damage?.length ? spell.damage : undefined,
|
|
effect: `${spell.description} (Tier ${spell.tier})`
|
|
};
|
|
create_attack_from_action(spellAction);
|
|
}
|
|
catch (ex) {
|
|
console.log(`Failed to add spell ${spellName}\n${ex}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// add bonuses (non-talents, species bonuses, etc.)
|
|
if (sheet.bonuses?.length) {
|
|
sheet.bonuses
|
|
.filter(o => o.sourceCategory !== "Talent")
|
|
.forEach(o => {
|
|
if (o.name?.length) {
|
|
|
|
try {
|
|
create_trait_from_feature({
|
|
name: addSpaces(o.name),
|
|
description: `${o.sourceName} ${o.sourceType} ${o.sourceCategory}: ${addSpaces(o.bonusName)}`
|
|
});
|
|
}
|
|
catch (ex) {
|
|
console.log(`Failed to import bonus o.name\n${ex}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// add ledger - hide details to avoid scrolling
|
|
if (sheet.ledger?.length) {
|
|
const hideDetails = true;
|
|
try {
|
|
create_trait_from_feature({
|
|
name: "Ledger",
|
|
description: sheet.ledger
|
|
.map(o => {
|
|
let details = [];
|
|
if (o.goldChange !== 0) {
|
|
let sign = o.goldChange < 0 ? "" : "+";
|
|
details.push(`${sign}${o.goldChange} GP`);
|
|
}
|
|
if (o.silverChange !== 0) {
|
|
let sign = o.silverChange < 0 ? "" : "+";
|
|
details.push(`${sign}${o.silverChange} SP`);
|
|
}
|
|
if (o.copperChange !== 0) {
|
|
let sign = o.copperChange < 0 ? "" : "+";
|
|
details.push(`${sign}${o.copperChange} CP`);
|
|
}
|
|
if (o.notes?.length) {
|
|
details.push((`(${o.notes})`));
|
|
}
|
|
if (details.length) {
|
|
return `${o.desc}: ${details.join(" ")}`;
|
|
}
|
|
return o.desc;
|
|
})
|
|
.join("\n")
|
|
}, hideDetails);
|
|
}
|
|
catch (ex) {
|
|
console.log(`Failed to import ledger\n${ex}`);
|
|
}
|
|
}
|
|
|
|
// add bonuses (talents only)
|
|
if (sheet.bonuses) {
|
|
let hideDetails = false;
|
|
sheet.bonuses
|
|
.filter(o => o.sourceCategory === "Talent")
|
|
.forEach(o => {
|
|
let talentDesc = '';
|
|
|
|
// if find matching ambition talent, use rolled name instead of bonusName
|
|
if (sheet.ambitionTalentLevel?.talentRolledName === o.bonusName) {
|
|
talentDesc = sheet.ambitionTalentLevel.talentRolledDesc;
|
|
}
|
|
if (!talentDesc.length) {
|
|
// default to bonusName (which is a key) but tokenize camel cased words to make it semi readable
|
|
talentDesc = addSpaces(o.bonusName);
|
|
}
|
|
|
|
try {
|
|
create_trait_from_feature({
|
|
name: addSpaces(o.name),
|
|
description: `Level ${o.gainedAtLevel} Talent: ${talentDesc}`
|
|
}, hideDetails);
|
|
}
|
|
catch (ex) {
|
|
console.log(`Failed to import bonus o.name\n${ex}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
update["show_options"] = "off"; // automatically hide options upon successfuly import
|
|
|
|
setAttrs(update, {
|
|
silent: true
|
|
});
|
|
update_slots(sheet.gearSlotsTotal);
|
|
|
|
// try to update name separately and set silent: false to try to fix popout window issue
|
|
if (sheet.name) {
|
|
try {
|
|
setAttrs({
|
|
character_name: sheet.name
|
|
}, {
|
|
silent: false
|
|
});
|
|
} catch (ex) {
|
|
console.log(`Failed to change sheet attr_character_name to ${sheet.name}`);
|
|
console.log(ex);
|
|
}
|
|
}
|
|
|
|
console.log("Finished importing custom sheet");
|
|
|
|
if (isNpc) {
|
|
importMonsterActions(sheet);
|
|
}
|
|
}
|
|
|
|
let importMonsterActions = function (monster) {
|
|
if (monster.attacks?.length) {
|
|
// add attackText as a "Multiple Attacks" feature if has multiple possible attacks or > 1 qty of single attack
|
|
if (monster.attackText?.length &&
|
|
(monster.attacks?.length > 1 || monster.attacks.filter(o => o.qty > 1).length)) {
|
|
create_trait_from_feature({ name: "Multiple Attacks", description: monster.attackText });
|
|
}
|
|
monster.attacks.filter(o => o.damage || o.damage2).forEach(o => {
|
|
try { create_attack_from_action(o); }
|
|
catch (ex) { console.log(`Failed to add attack ${o}\n${ex}`); }
|
|
});
|
|
}
|
|
if (monster.actions?.length) {
|
|
monster.actions.forEach(o => {
|
|
try { create_trait_from_feature(o); }
|
|
catch (ex) { console.log(`Failed to add action ${o}\n${ex}`); }
|
|
});
|
|
}
|
|
}
|
|
|
|
let addSpaces = function (str) {
|
|
return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([a-zA-Z])([0-9])/g, "$1 $2");
|
|
}
|
|
|
|
core_stats.forEach(attr => {
|
|
on(`change:${attr}_base change:${attr}_bonus`, function () {
|
|
update_attr(`${attr}`);
|
|
});
|
|
});
|
|
|
|
core_stats.forEach(attr => {
|
|
on(`change:${attr}`, function () {
|
|
update_mod(`${attr}`);
|
|
const cap = attr.charAt(0).toUpperCase() + attr.slice(1);
|
|
(attr === "dexterity") ? update_initiative() : false;
|
|
});
|
|
});
|
|
|
|
core_stats.forEach(attr => {
|
|
on(`change:${attr}_mod`, function () {
|
|
update_attacks(`${attr}`);
|
|
switch (`${attr}`) {
|
|
case "strength":
|
|
// decided to not auto-update total slots because of logic for copyrighted class abilities
|
|
console.log("Strength score changed, but you must manually update total slots.");
|
|
break;
|
|
case "dexterity":
|
|
update_initiative();
|
|
break;
|
|
default:
|
|
false;
|
|
}
|
|
});
|
|
});
|
|
|
|
on("change:advantagetoggle", function (eventinfo) {
|
|
if (eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
|
|
return;
|
|
}
|
|
update_initiative();
|
|
});
|
|
|
|
on("change:core_die", function () {
|
|
getAttrs(["core_die"], function (v) {
|
|
let core = v.core_die && v.core_die != "" ? v.core_die : "1d20";
|
|
let update = { "d20": core };
|
|
if (!v.core_die || v.core_die === "") {
|
|
update["core_die"] = "1d20";
|
|
}
|
|
setAttrs(update);
|
|
});
|
|
});
|
|
|
|
on("change:repeating_attack:atkname change:repeating_attack:atkattr_base change:repeating_attack:atkmod change:repeating_attack:atkmagic change:repeating_attack:dmgflag change:repeating_attack:dmgbase change:repeating_attack:dmgmod change:repeating_attack:dmgtype change:repeating_attack:dmg2flag change:repeating_attack:dmg2base change:repeating_attack:dmg2mod change:repeating_attack:dmg2type change:repeating_attack:dmgcustcrit change:repeating_attack:dmg2custcrit change:repeating_attack:castdc change:repeating_attack:duration change:repeating_attack:atkrange", function (eventinfo) {
|
|
if (eventinfo.sourceType === "sheetworker") {
|
|
return;
|
|
}
|
|
var source = eventinfo.sourceAttribute.substr(38);
|
|
var attackid = eventinfo.sourceAttribute.substring(17, 37);
|
|
update_attacks(attackid);
|
|
});
|
|
|
|
on("change:dtype", function (eventinfo) {
|
|
if (eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
|
|
return;
|
|
}
|
|
update_attacks("all");
|
|
});
|
|
|
|
on("change:base_level", function (eventinfo) {
|
|
if (eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
|
|
return;
|
|
}
|
|
console.log(eventinfo);
|
|
let level = parseInt(+eventinfo.newValue, 10) || 0;
|
|
let oldLevel = parseInt(+eventinfo.previousValue, 10) || 0;
|
|
set_level(level, oldLevel);
|
|
});
|
|
|
|
on("change:initmod change:init_tiebreaker", function (eventinfo) {
|
|
if (eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
|
|
return;
|
|
}
|
|
update_initiative();
|
|
});
|
|
|
|
on("change:slots_total", function (eventinfo) {
|
|
// disable gear slots input based on slot capacity
|
|
if (eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
|
|
return;
|
|
}
|
|
update_slots(eventinfo.newValue);
|
|
});
|
|
|
|
let update_slots = function (totalSlots) {
|
|
let update = {};
|
|
const maxPcSlots = Math.max(minPcSlots, parseInt(totalSlots, 10) || minPcSlots);
|
|
if (totalSlots < minPcSlots) update["slots_total"] = minPcSlots;
|
|
// use separate checkbox for each slot which triggers CSS rule to 'disable' it (really just blacks it out) but doesn't clear the value
|
|
for (let i = 1; i <= maxSheetSlots; ++i) {
|
|
let attrName = "gearslot" + i + "enabled";
|
|
update[attrName] = i <= maxPcSlots ? "1" : "0";
|
|
}
|
|
setAttrs(update);
|
|
}
|
|
|
|
var update_attr = function (attr) {
|
|
var update = {};
|
|
var attr_fields = [attr + "_base", attr + "_bonus"];
|
|
getSectionIDs("repeating_inventory", function (idarray) {
|
|
_.each(idarray, function (currentID, i) {
|
|
attr_fields.push("repeating_inventory_" + currentID + "_equipped");
|
|
attr_fields.push("repeating_inventory_" + currentID + "_itemmodifiers");
|
|
});
|
|
getAttrs(attr_fields, function (v) {
|
|
var base = v[attr + "_base"] && !isNaN(parseInt(v[attr + "_base"], 10)) ? parseInt(v[attr + "_base"], 10) : 10;
|
|
var bonus = v[attr + "_bonus"] && !isNaN(parseInt(v[attr + "_bonus"], 10)) ? parseInt(v[attr + "_bonus"], 10) : 0;
|
|
var item_base = 0;
|
|
var item_bonus = 0;
|
|
_.each(idarray, function (currentID) {
|
|
if ((!v["repeating_inventory_" + currentID + "_equipped"] || v["repeating_inventory_" + currentID + "_equipped"] === "1") && v["repeating_inventory_" + currentID + "_itemmodifiers"] && v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().indexOf(attr > -1)) {
|
|
var mods = v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().split(",");
|
|
_.each(mods, function (mod) {
|
|
if (mod.indexOf(attr) > -1) {
|
|
if (mod.indexOf(":") > -1) {
|
|
var new_base = !isNaN(parseInt(mod.replace(/[^0-9]/g, ""), 10)) ? parseInt(mod.replace(/[^0-9]/g, ""), 10) : false;
|
|
item_base = new_base && new_base > item_base ? new_base : item_base;
|
|
} else if (mod.indexOf("-") > -1) {
|
|
var new_mod = !isNaN(parseInt(mod.replace(/[^0-9]/g, ""), 10)) ? parseInt(mod.replace(/[^0-9]/g, ""), 10) : false;
|
|
item_bonus = new_mod ? item_bonus - new_mod : item_bonus;
|
|
} else {
|
|
var new_mod = !isNaN(parseInt(mod.replace(/[^0-9]/g, ""), 10)) ? parseInt(mod.replace(/[^0-9]/g, ""), 10) : false;
|
|
item_bonus = new_mod ? item_bonus + new_mod : item_bonus;
|
|
}
|
|
};
|
|
});
|
|
}
|
|
});
|
|
|
|
base = base > item_base ? base : item_base;
|
|
update[attr] = base + bonus + item_bonus;
|
|
setAttrs(update);
|
|
});
|
|
});
|
|
};
|
|
|
|
var update_mod = function (attr) {
|
|
getAttrs([attr, "npc"], function (v) {
|
|
var attr_abr = attr.substring(0, 3);
|
|
// per SD rules attributes can exceed 18 but mods can"t exceed +4, except for monsters/NPCs
|
|
const modMax = v.npc === "1" ? 99 : 4;
|
|
// todo: add option to toggle enforcing the PC stat mod limit
|
|
var finalattr = v[attr] && isNaN(v[attr]) === false ? Math.floor((parseInt(v[attr], 10) - 10) / 2) : 0;
|
|
var update = {};
|
|
update[attr + "_mod"] = Math.min(modMax, finalattr);
|
|
setAttrs(update);
|
|
});
|
|
};
|
|
|
|
var update_attacks = function (update_id, source) {
|
|
console.log("DOING UPDATE_ATTACKS: " + update_id);
|
|
if (update_id.substring(0, 1) === "-" && update_id.length === 20) {
|
|
do_update_attack([update_id], source);
|
|
} else if (["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma", "all"].indexOf(update_id) > -1) {
|
|
getSectionIDs("repeating_attack", function (idarray) {
|
|
if (update_id === "all") {
|
|
do_update_attack(idarray);
|
|
} else {
|
|
var attack_attribs = [];
|
|
_.each(idarray, function (id) {
|
|
attack_attribs.push("repeating_attack_" + id + "_atkattr_base");
|
|
});
|
|
getAttrs(attack_attribs, function (v) {
|
|
var attr_attack_ids = [];
|
|
_.each(idarray, function (id) {
|
|
if ((v["repeating_attack_" + id + "_atkattr_base"] && v["repeating_attack_" + id + "_atkattr_base"].indexOf(update_id) > -1)) {
|
|
attr_attack_ids.push(id);
|
|
}
|
|
});
|
|
if (attr_attack_ids.length > 0) {
|
|
do_update_attack(attr_attack_ids);
|
|
}
|
|
});
|
|
};
|
|
});
|
|
};
|
|
};
|
|
|
|
var do_update_attack = function (attack_array, source) {
|
|
var attack_attribs = ["level", "d20", "dtype", "strength_mod", "dexterity_mod", "constitution_mod", "intelligence_mod", "wisdom_mod", "charisma_mod"];
|
|
_.each(attack_array, function (attackid) {
|
|
attack_attribs.push("repeating_attack_" + attackid + "_atkname");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_atkattr_base");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_atkmod");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_atkmagic");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_dmgflag");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_dmgbase");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_dmgmod");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_dmgtype");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_dmg2flag");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_dmg2base");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_dmg2mod");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_dmg2type");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_dmgcustcrit");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_dmg2custcrit");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_atkrange");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_duration");
|
|
attack_attribs.push("repeating_attack_" + attackid + "_castdc");
|
|
});
|
|
|
|
getAttrs(attack_attribs, function (v) {
|
|
_.each(attack_array, function (attackid) {
|
|
var callbacks = [];
|
|
var update = {};
|
|
var hbonus = "";
|
|
var hdmg1 = "";
|
|
var hdmg2 = "";
|
|
var dmg = "";
|
|
var dmg2 = "";
|
|
var rollbase = "";
|
|
var magicattackmod = 0;
|
|
var atkattr_abrev = "";
|
|
|
|
if (!v["repeating_attack_" + attackid + "_atkattr_base"] || v["repeating_attack_" + attackid + "_atkattr_base"] === "0") {
|
|
atkattr_base = 0;
|
|
} else {
|
|
atkattr_base = parseInt(v[v["repeating_attack_" + attackid + "_atkattr_base"].substring(2, v["repeating_attack_" + attackid + "_atkattr_base"].length - 1)], 10);
|
|
atkattr_abrev = v["repeating_attack_" + attackid + "_atkattr_base"].substring(2, 5).toUpperCase();
|
|
};
|
|
|
|
var dmgbase = v["repeating_attack_" + attackid + "_dmgbase"] && v["repeating_attack_" + attackid + "_dmgbase"] != "" ? v["repeating_attack_" + attackid + "_dmgbase"] : 0;
|
|
var dmg2base = v["repeating_attack_" + attackid + "_dmg2base"] && v["repeating_attack_" + attackid + "_dmg2base"] != "" ? v["repeating_attack_" + attackid + "_dmg2base"] : 0;
|
|
var dmgmod = v["repeating_attack_" + attackid + "_dmgmod"] && isNaN(parseInt(v["repeating_attack_" + attackid + "_dmgmod"], 10)) === false ? parseInt(v["repeating_attack_" + attackid + "_dmgmod"], 10) : 0;
|
|
var dmg2mod = v["repeating_attack_" + attackid + "_dmg2mod"] && isNaN(parseInt(v["repeating_attack_" + attackid + "_dmg2mod"], 10)) === false ? parseInt(v["repeating_attack_" + attackid + "_dmg2mod"], 10) : 0;
|
|
var dmgtype = v["repeating_attack_" + attackid + "_dmgtype"] ? v["repeating_attack_" + attackid + "_dmgtype"] + " " : "";
|
|
var dmg2type = v["repeating_attack_" + attackid + "_dmg2type"] ? v["repeating_attack_" + attackid + "_dmg2type"] + " " : "";
|
|
var atkmod = v["repeating_attack_" + attackid + "_atkmod"] && v["repeating_attack_" + attackid + "_atkmod"] != "" ? parseInt(v["repeating_attack_" + attackid + "_atkmod"], 10) : 0;
|
|
var atkmag = v["repeating_attack_" + attackid + "_atkmagic"] && v["repeating_attack_" + attackid + "_atkmagic"] != "" ? parseInt(v["repeating_attack_" + attackid + "_atkmagic"], 10) : 0;
|
|
var dmgmag = isNaN(atkmag) === false && atkmag != 0 && ((v["repeating_attack_" + attackid + "_dmgflag"] && v["repeating_attack_" + attackid + "_dmgflag"] != 0) || (v["repeating_attack_" + attackid + "_dmg2flag"] && v["repeating_attack_" + attackid + "_dmg2flag"] != 0)) ? "+ " + atkmag : "";
|
|
bonus_mod = atkattr_base + atkmod + atkmag + magicattackmod;
|
|
bonus_mod = bonus_mod;
|
|
plus_minus = bonus_mod > -1 ? "+" : "";
|
|
bonus = plus_minus + bonus_mod;
|
|
|
|
if (v["repeating_attack_" + attackid + "_dmgflag"] && v["repeating_attack_" + attackid + "_dmgflag"] != 0) {
|
|
if (dmgbase === 0 && dmgmod === 0) {
|
|
dmg = 0;
|
|
}
|
|
if (dmgbase != 0) {
|
|
dmg = dmgbase;
|
|
}
|
|
if (dmgbase != 0 && dmgmod != 0) {
|
|
dmg = dmgmod > 0 ? dmg + "+" : dmg;
|
|
}
|
|
if (dmgmod != 0) {
|
|
dmg = dmg + dmgmod;
|
|
}
|
|
dmg = dmg + " " + dmgtype;
|
|
} else {
|
|
dmg = "";
|
|
};
|
|
if (v["repeating_attack_" + attackid + "_dmg2flag"] && v["repeating_attack_" + attackid + "_dmg2flag"] != 0) {
|
|
if (dmg2base === 0 && dmg2mod === 0) {
|
|
dmg2 = 0;
|
|
}
|
|
if (dmg2base != 0) {
|
|
dmg2 = dmg2base;
|
|
}
|
|
if (dmg2base != 0 && dmg2mod != 0) {
|
|
dmg2 = dmg2mod > 0 ? dmg2 + "+" : dmg2;
|
|
}
|
|
if (dmg2mod != 0) {
|
|
dmg2 = dmg2 + dmg2mod;
|
|
}
|
|
dmg2 = dmg2 + " " + dmg2type;
|
|
} else {
|
|
dmg2 = "";
|
|
};
|
|
dmgspacer = v["repeating_attack_" + attackid + "_dmgflag"] && v["repeating_attack_" + attackid + "_dmgflag"] != 0 && v["repeating_attack_" + attackid + "_dmg2flag"] && v["repeating_attack_" + attackid + "_dmg2flag"] != 0 ? "+ " : "";
|
|
crit1 = v["repeating_attack_" + attackid + "_dmgcustcrit"] && v["repeating_attack_" + attackid + "_dmgcustcrit"] != "" ? v["repeating_attack_" + attackid + "_dmgcustcrit"] : dmgbase;
|
|
crit2 = v["repeating_attack_" + attackid + "_dmg2custcrit"] && v["repeating_attack_" + attackid + "_dmg2custcrit"] != "" ? v["repeating_attack_" + attackid + "_dmg2custcrit"] : dmg2base;
|
|
r1 = "@{d20}";
|
|
r2 = "@{rtype}";
|
|
if (atkmag != 0) {
|
|
hbonus = " + " + atkmag + "[MAGIC]" + hbonus
|
|
};
|
|
if (atkmod != 0) {
|
|
hbonus = " + " + atkmod + "[MOD]" + hbonus
|
|
};
|
|
if (atkattr_base != 0) {
|
|
hbonus = " + " + atkattr_base + "[" + atkattr_abrev + "]" + hbonus
|
|
};
|
|
if (v["repeating_attack_" + attackid + "_dmgflag"] && v["repeating_attack_" + attackid + "_dmgflag"] != 0) {
|
|
if (atkmag != 0) {
|
|
hdmg1 = " + " + atkmag + "[MAGIC]" + hdmg1
|
|
};
|
|
if (dmgmod != 0) {
|
|
hdmg1 = " + " + dmgmod + "[MOD]" + hdmg1
|
|
};
|
|
hdmg1 = dmgbase + hdmg1;
|
|
} else {
|
|
hdmg1 = "0";
|
|
}
|
|
if (v["repeating_attack_" + attackid + "_dmg2flag"] && v["repeating_attack_" + attackid + "_dmg2flag"] != 0) {
|
|
if (dmg2mod != 0) {
|
|
hdmg2 = " + " + dmg2mod + "[MOD]" + hdmg2
|
|
};
|
|
hdmg2 = dmg2base + hdmg2;
|
|
} else {
|
|
hdmg2 = "0";
|
|
}
|
|
pickbase = "pick";
|
|
rollbase = "@{wtype}&{template:atk} {{mod=@{atkbonus}}} {{rname=[@{atkname}](~repeating_attack_attack_dmg)}} {{rnamec=[@{atkname}](~repeating_attack_attack_crit)}} {{r1=[[" + r1 + "cs>@{atkcritrange}" + hbonus + "]]}} " + r2 + "cs>@{atkcritrange}" + hbonus + "]]}} {{range=@{atkrange}}} {{desc=@{atk_desc}}} {{castdc=@{castdc}}} @{charname_output}";
|
|
|
|
// auto roll damage also, requires its own roll template
|
|
if (v.dtype === "full") {
|
|
pickbase = "full";
|
|
let hldmgcrit = v["repeating_attack_" + attackid + "_hldmg"] && v["repeating_attack_" + attackid + "_hldmg"] != "" ? v["repeating_attack_" + attackid + "_hldmg"].slice(0, 7) + "crit" + v["repeating_attack_" + attackid + "_hldmg"].slice(7) : "";
|
|
rollbase = "@{wtype}&{template:atkdmg} {{attack=1}} {{damage=1}} {{mod=@{atkbonus}}} {{rname=@{atkname}}} {{r1=[[" + r1 + "cs>@{atkcritrange}" + hbonus + "]]}} " + r2 + "cs>@{atkcritrange}" + hbonus + "]]}} {{range=@{atkrange}}} {{duration=@{duration}}} @{dmgflag} {{dmg1=[[" + hdmg1 + "]]}} {{dmg1type=" + dmgtype + "}} @{dmg2flag} {{dmg2=[[" + hdmg2 + "]]}} {{dmg2type=" + dmg2type + "}} {{crit1=[[" + crit1 + "[CRIT]]]}} {{crit2=[[" + crit2 + "[CRIT]]]}} {{desc=@{atk_desc}}} @{hldmg} " + hldmgcrit + " {{castdc=@{castdc}}} @{charname_output}";
|
|
}
|
|
|
|
update["repeating_attack_" + attackid + "_rollbase_dmg"] = "@{wtype}&{template:dmg} {{rname=@{atkname}}} {{range=@{atkrange}}} {{duration=@{duration}}} @{dmgflag} {{dmg1=[[" + hdmg1 + "]]}} {{dmg1type=" + dmgtype + "}} @{dmg2flag} {{dmg2=[[" + hdmg2 + "]]}} {{dmg2type=" + dmg2type + "}} {{desc=@{atk_desc}}} @{charname_output}";
|
|
update["repeating_attack_" + attackid + "_rollbase_crit"] = "@{wtype}&{template:dmg} {{crit=1}} {{rname=@{atkname}}} {{range=@{atkrange}}} {{duration=@{duration}}} @{dmgflag} {{dmg1=[[" + hdmg1 + "]]}} {{dmg1type=" + dmgtype + "}} @{dmg2flag} {{dmg2=[[" + hdmg2 + "]]}} {{dmg2type=" + dmg2type + "}} {{crit1=[[" + crit1 + "]]}} {{crit2=[[" + crit2 + "]]}} {{desc=@{atk_desc}}} @{charname_output}";
|
|
update["repeating_attack_" + attackid + "_atkbonus"] = bonus;
|
|
update["repeating_attack_" + attackid + "_atkdmgtype"] = dmg + dmgspacer + dmg2 + dmgmag + " ";
|
|
update["repeating_attack_" + attackid + "_rollbase"] = rollbase;
|
|
setAttrs(update, {
|
|
silent: true
|
|
}, function () {
|
|
callbacks.forEach(function (callback) {
|
|
callback();
|
|
})
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
var update_attack_from_action = function (action, attackId, options) {
|
|
const update = {};
|
|
const prefix = "repeating_attack_" + attackId;
|
|
if (options?.new) {
|
|
// hide edit form by default
|
|
update[`${prefix}_options-flag`] = "0";
|
|
}
|
|
//attr_name attr_description
|
|
update[`${prefix}_atkname`] = action.name;
|
|
update[`${prefix}_atkattr_base`] = action.baseAttr || "0"; // monster attacks use mod only, not stats
|
|
if (action.mod !== undefined && +action.mod !== 0) update[`${prefix}_atkmod`] = action.mod.toString();
|
|
update[`${prefix}_duration`] = action.duration || "";
|
|
update[`${prefix}_castdc`] = action.dc || "";
|
|
update[`${prefix}_atkrange`] = action.range || "";
|
|
update[`${prefix}_equipped`] = "1"; // default to usable state
|
|
update[`${prefix}_fixed`] = "1"; // default to fixed state
|
|
if (action.magBonus !== undefined && +action.magBonus !== 0) update[`${prefix}_atkmagic`] = action.magBonus.toString();
|
|
update[`${prefix}_atkcritrange`] = action.critRange || "20";
|
|
update[`${prefix}_dmgflag`] = action.damage?.length ? "{{damage=1}} {{dmg1flag=1}}" : "{{damage=0}} {{dmg1flag=0}}";
|
|
update[`${prefix}_dmgbase`] = action.damage?.length ? action.damage : "";
|
|
if (action.damageMod !== undefined && +action.damageMod !== 0) update[`${prefix}_dmgmod`] = action.damageMod.toString();
|
|
update[`${prefix}_dmgcustcrit`] = action.damageCustomCrit || "";
|
|
update[`${prefix}_dmg2flag`] = action.damage2 === undefined ? "" : "{{damage=1}} {{dmg2flag=1}}";
|
|
update[`${prefix}_dmg2base`] = action.damage2 || "";
|
|
update[`${prefix}_dmg2mmod`] = action.damage2Mod || "";
|
|
update[`${prefix}_dmg2custcrit`] = action.damage2CustomCrit || "";
|
|
update[`${prefix}_atk_desc`] = action.effect || "";
|
|
setAttrs(update, {
|
|
silent: true
|
|
}, () => update_attacks(attackId));
|
|
};
|
|
|
|
var update_trait_from_feature = function (feature, traitId, options) {
|
|
const update = {};
|
|
const prefix = "repeating_trait_" + traitId;
|
|
if (options?.new) {
|
|
// hide edit form by default
|
|
update[`${prefix}_options-flag`] = "0";
|
|
}
|
|
//attr_name attr_description
|
|
update[`${prefix}_hide_details`] = options?.hide ? "1" : "0";
|
|
update[`${prefix}_name`] = feature.name || "";
|
|
update[`${prefix}_description`] = feature.description || "";
|
|
setAttrs(update, {
|
|
silent: true
|
|
});
|
|
};
|
|
|
|
let create_trait_from_feature = function (feature, hideDetails) {
|
|
var update = {};
|
|
var newrowid = generateRowID();
|
|
setAttrs(update, {}, update_trait_from_feature(feature, newrowid, {
|
|
new: true,
|
|
hide: hideDetails === undefined ? false : hideDetails
|
|
}));
|
|
}
|
|
|
|
let create_attack_from_action = function (action) {
|
|
var update = {};
|
|
var newrowid = generateRowID();
|
|
setAttrs(update, {}, update_attack_from_action(action, newrowid, {
|
|
new: true
|
|
}));
|
|
};
|
|
|
|
var update_initiative = function () {
|
|
var attrs_to_get = ["dexterity", "dexterity_mod", "init_tiebreaker", "advantagetoggle"];
|
|
getAttrs(attrs_to_get, function (v) {
|
|
var update = {};
|
|
var final_init = parseInt(v["dexterity_mod"], 10);
|
|
if (v["init_tiebreaker"] != "0") {
|
|
final_init = (final_init + parseFloat(Math.abs(parseInt(v["dexterity"], 10) / 100))).toFixed(2);
|
|
}
|
|
|
|
switch (v["advantagetoggle"]) {
|
|
// advantage
|
|
case "{{query=1}} {{advantage=1}} {{r2=[[@{d20}":
|
|
update["initiative_style"] = "{@{d20},@{d20}}kh1";
|
|
break;
|
|
// disadvantage
|
|
case "{{query=1}} {{disadvantage=1}} {{r2=[[@{d20}":
|
|
update["initiative_style"] = "{@{d20},@{d20}}kl1";
|
|
break;
|
|
// normal
|
|
default:
|
|
update["initiative_style"] = "@{d20}";
|
|
break;
|
|
}
|
|
|
|
update["initiative_bonus"] = final_init;
|
|
setAttrs(update, {
|
|
silent: true
|
|
});
|
|
});
|
|
};
|
|
|
|
on("change:xp", function (eventinfo) {
|
|
getAttrs(["lifetime_xp", "base_level"], function (v) {
|
|
let xp = parseInt(eventinfo.newValue, 10) || 0;
|
|
let update = {};
|
|
if (xp < 0) { xp = 0; update["xp"] = xp; }
|
|
if (xp > maxTotalXp) { xp = maxTotalXp; update["xp"] = xp; }
|
|
let level = Math.max(0, parseInt(v.base_level, 10) || minLevel);
|
|
let lifetimeXp = xp + getTotalXpForLevel(level);
|
|
let ascended = level > minLevel && level < maxLevel && xp >= level * 10;
|
|
console.log("current xp is now: " + xp + "; lifetime XP is now: " + lifetimeXp + "; ascended: " + ascended);
|
|
update["lifetime_xp"] = lifetimeXp;
|
|
update["levelup"] = ascended ? 1 : 0; // set level up readiness indicator
|
|
setAttrs(update, {
|
|
silent: true
|
|
});
|
|
});
|
|
});
|
|
|
|
// derive XP needed to achieve a specific level
|
|
var getTotalXpForLevel = function (level) {
|
|
// base cases
|
|
if (level <= 1) return 0;
|
|
// limit = level 10 (for now ? not counting 3rd party stuff)
|
|
if (level > 10) return getTotalXpForLevel(10);
|
|
// recurse
|
|
return getTotalXpForLevel(level - 1) + (level - 1) * 10;
|
|
}
|
|
|
|
// update XP for next level when level is changed
|
|
var set_level = function (level, oldLevel) {
|
|
getAttrs(["xp", "lifetime_xp"], function (v) {
|
|
console.log("level changed (" + oldLevel + "=>" + level + ") reset xp and adjust xp for next level");
|
|
var update = {};
|
|
const minLevel = 0;
|
|
const maxLevel = 10;
|
|
const maxLifetimeXp = 450;
|
|
if (level < minLevel) { level = minLevel; update["base_level"] = level; };
|
|
|
|
// SD uses next level xp equals cur level * 10
|
|
let lastLevelXp = level > minLevel + 1 ? (level - 1) * 10 : 0;
|
|
let nextLevelXp = level < maxLevel ? level * 10 : 0;
|
|
let xp = Math.max(0, parseInt(v.xp, 10) || 0);
|
|
let lifetimeXp = (v.lifetime_xp && v.lifetime_xp > 0) ? parseInt(v.lifetime_xp, 10) : 0;
|
|
if (lifetimeXp > maxLifetimeXp) { lifetimeXp = maxLifetimeXp; update["lifetime_xp"] = lifetimeXp; };
|
|
if (level < oldLevel) {
|
|
// level down: fix xp to avoid losing total xp (people make mistakes)
|
|
xp = lifetimeXp - getTotalXpForLevel(level);
|
|
}
|
|
else if (level > 1) {
|
|
if (level >= maxLevel) console.log("Maximum level achieved!");
|
|
if (xp >= lastLevelXp) {
|
|
// normal level up based on XP (assume player didn"t cheat; track total xp in case of mistakes)
|
|
// reset XP to 0 and carry over remainder if exceeded required amount
|
|
xp = level < maxLevel && xp >= lastLevelXp ? xp - (oldLevel * 10) : 0;
|
|
}
|
|
else {
|
|
// skip: level up without required xp (milestone leveling or starting a high level PC?)
|
|
// increase lifetime_xp to match, and set current to 0
|
|
lifetimeXp = getTotalXpForLevel(level);
|
|
update["lifetime_xp"] = lifetimeXp;
|
|
xp = 0;
|
|
}
|
|
}
|
|
|
|
update["levelup"] = xp >= nextLevelXp ? 1 : 0; // set level up readiness indicator
|
|
update["xp"] = xp;
|
|
update["xp_nextlevel"] = nextLevelXp;
|
|
setAttrs(update, {
|
|
silent: true
|
|
});
|
|
});
|
|
};
|
|
const classCastingStat = { "wizard": "intelligence", "priest": "wisdom", "seer": "wisdom" };
|
|
const compendium = {
|
|
"version": "2025.9.7.0",
|
|
"thirdPartyLicenseText": "The Shadowdark JSON Project is an independent product published under the Shadowdark RPG Third-Party License and is not affiliated with The Arcane Library, LLC. Shadowdark RPG © 2023 The Arcane Library, LLC.",
|
|
"spells": [
|
|
{
|
|
"source": "core rules",
|
|
"name": "Acid Arrow",
|
|
"description": "You conjure a corrosive bolt that hits one foe, dealing 1d6 damage a round. The bolt remains in the target for as long as you focus.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Far",
|
|
"damage": "1d6",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Alarm",
|
|
"description": "You touch one object, such as a door threshold, setting a magical alarm on it. If any creature you do not designate while casting the spell touches or crosses past the object, a magical bell sounds in your head.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "1 day",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Alter Self",
|
|
"description": "You magically change your physical form, gaining one feature that modifies your existing anatomy.\n\nFor example, you can grow functional gills on your neck or bear claws on your fingers. This spell can't grow wings or limbs.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Animate Dead",
|
|
"description": "You touch one humanoid's remains, and it rises as a zombie or skeleton under your control. The remains must have at least three limbs and its head intact.The undead creature acts on your turn. After 1 day, the creature collapses into grave dust.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "1 day",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Antimagic Shell",
|
|
"description": "An invisible, near-sized cube of null-magic appears centered on you.\n\nWithin the cube, no spells can be cast. Magic items and spells have no effect in the zone, and no magic can enter.\n\nThe cube moves with you. Spells such as dispel magic have no effect on it.\n\nAnother antimagic shell does not affect this one.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Arcane Eye",
|
|
"description": "You conjure an invisible, grape- sized eye within range.\n\nYou can see through the eye. It can see in the dark out to near range, fly near on your turn, and squeeze through openings as narrow as a keyhole.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Augury",
|
|
"description": "You interpret the meaning of supernatural portents and omens.\n\nAsk the GM one question about a specific course of action. The GM says whether the action will lead to “weal” or “woe.”",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Bless",
|
|
"description": "One creature you touch gains a luck token.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Blind/deafen",
|
|
"description": "You utter a divine censure, blinding or deafening one creature you can see in range.The creature has disadvantage on tasks requiring the lost sense.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Burning Hands",
|
|
"description": "You spread your fingers with thumbs touching, unleashing a circle of flame that roars out to a close area around where you stand.\n\nCreatures within the area of effect take 1d6 damage, and flammable objects catch fire.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Close",
|
|
"damage": "1d6",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Charm Person",
|
|
"description": "You magically beguile one humanoid of level 2 or less within near range, who regards you as a friend for the duration.\n\nThe spell ends if you or your allies do anything to hurt it that it notices.\n\nThe target knows you magically enchanted it after the spell ends.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "1d8 days",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Cleansing Weapon",
|
|
"description": "One weapon you touch is wreathed in purifying flames. It deals an additional 1d4 damage (1d6 vs. undead) for the duration.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Close",
|
|
"damage": "1d4",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Cloudkill",
|
|
"description": "A putrid cloud of yellow poison fills a near-sized cube within range. It spreads around corners.\n\nCreatures inside the cloud are blinded and take 2d6 damage at the beginning of their turns.A creature of LV 9 or less that ends its turn fully inside the cloud dies.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Far",
|
|
"damage": "2d6",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Command",
|
|
"description": "You issue a verbal command to one creature in range who can understand you. The command must be one word, such as “kneel.” The target obeys the command for as long as you focus.\n\nIf your command is ever directly harmful to the creature, it may make a CHA check vs. your last spellcasting check. On a success, the spell ends.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"resistCheck": "CHA",
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Far",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Commune",
|
|
"description": "You seek your god's counsel. Ask the GM up to three yes or no questions. The GM truthfully answers \"yes\" or \"no\" to each.\n\nIf you cast this spell more than once in 24 hours, treat a failed spellcasting check for it as a critical failure instead.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Confusion",
|
|
"description": "You mesmerize one creature you can see in range. The target can't take actions, and it moves in a random direction on its turn. If the target is LV 9+, it may make a WIS check vs. your last spellcasting check at the start of its turn to end the spell.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"resistCheck": "WIS",
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Control Water",
|
|
"description": "You move and shape water. You can cause a section of water up to 100 feet in width and depth to change shape, defy gravity, or flow in a different direction.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"priest",
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Far",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Create Undead",
|
|
"description": "You conjure a vengeful undead creature to do your bidding.\n\nWhen you cast this spell, you choose to summon either a wight or wraith. It appears next to you and is under your control.\n\nThe undead creature acts on your turn. After 1 day, it melts away into smoke.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "1 day",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Cure Wounds",
|
|
"description": "Your touch restores ebbing life.\n\nRoll a number of d6s equal to 1 + half your level (rounded down). One target you touch regains that many hit points.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Detect Magic",
|
|
"description": "You can sense the presence of magic within near range for the spell's duration. If you focus for two rounds, you discern its general properties. Full barriers block this spell.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Detect Thoughts",
|
|
"description": "",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard Duration: Focus"
|
|
],
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Dimension Door",
|
|
"description": "You teleport yourself and up to one other willing creature within close to any point you can see.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Disintegrate",
|
|
"description": "A green ray shoots from your finger and turns a creature or object into ash.\n\nA target creature of LV 5 or less instantly dies. If it is LV 6+, it takes 3d8 damage instead.\n\nA non-magical object up to the size of a large tree is destroyed.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Far",
|
|
"damage": "3d8",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Dispel Magic",
|
|
"description": "End one spell that affects one target you can see in range.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Divination",
|
|
"description": "You throw the divining bones or peer into the blackness between the stars, seeking a portent.\n\nYou can ask the GM one yes or no question. The GM truthfully answers \"yes\" or \"no.\" \n\nIf you cast this spell more than once in 24 hours, treat a failed spellcasting check for it as a critical failure instead.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Divine Vengeance",
|
|
"description": "You become the divine avatar of your god's wrath, wreathed in holy flames or a black aura of smoldering corruption.For the spell's duration, you can fly a near distance, your weapons are magical, and you have a +4 bonus to your weapon attacks and damage.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Dominion",
|
|
"description": "Mighty beings come to your aid.\n\nThe beings must have a combined total of 16 levels or less. Chaotic PCs summon demons/devils, and lawful or neutral PCs summon angels.\n\nThe beings act of free will to aid you on your turn. After 10 rounds, they return to their realms.\n\nYou cannot cast this spell again until you complete penance.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Fabricate",
|
|
"description": "This spell can't target creatures.You turn a tree-sized collection of raw materials into a finished work. For example, you convert a pile of bricks or rocks into a bridge. The finished work converts back to raw materials when the spell ends.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Feather Fall",
|
|
"description": "You may make an attempt to cast this spell when you fall.\n\nYour rate of descent slows so that you land safely on your feet.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Fireball",
|
|
"description": "You hurl a small flame that erupts into a fiery blast. All creatures in a near-sized cube around where the flame lands take 4d6 damage.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Far",
|
|
"damage": "4d6",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Fixed Object",
|
|
"description": "An object you touch that weighs no more than 5 pounds becomes fixed in its current location. It can support up to 5,000 pounds of weight for the duration of the spell.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Flame Strike",
|
|
"description": "You call down a holy pillar of fire, immolating one creature you can see within range. The target takes 2d6 damage.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Far",
|
|
"damage": "2d6",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Floating Disk",
|
|
"description": "You create a floating, circular disk of force with a concave center. It can carry up to 20 gear slots. It hovers at waist level and automatically stays within near of you. It can't cross over drop- offs or pits taller than a human.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Gaseous Form",
|
|
"description": "You and your gear turn into a cloud of smoke for the spell's duration.\n\nYou can fly and pass through any gap that smoke could. You can sense the terrain and any movement around you out to a near distance.\n\nYou can't cast spells while in this form.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Fly",
|
|
"description": "Your feet lift from the ground, and you take to the air like a hummingbird. You can fly near for the spell's duration and are able to hover in place.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Heal",
|
|
"description": "One creature you touch is healed to full hit points.You cannot cast this spell again until you complete a rest.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Hold Monster",
|
|
"description": "You paralyze one creature you can see within range.If the target is LV 9+, it may make a STR check vs. your last spellcasting check at the start of its turn to end the spell.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"resistCheck": "STR",
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Holy Weapon",
|
|
"description": "One weapon you touch is imbued with a sacred blessing. The weapon becomes magical and has +1 to attack and damage rolls for the duration.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Hold Person",
|
|
"description": "You magically paralyze one humanoid creature of LV 4 or less you can see within range.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Hold Portal",
|
|
"description": "You magically hold a portal closed for the duration. A creature must make a successful STR check vs. your spellcasting check to open the portal. The knock spell ends this spell.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"resistCheck": "STR",
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Illusion",
|
|
"description": "You create a convincing visible and audible illusion that fills up to a near-sized cube in range.\n\nThe illusion cannot cause harm, but creatures who believe the illusion is real react to it as though it were.\n\nA creature who inspects the illusion from afar must pass a WIS check vs. your last spellcasting check to perceive the false nature of the illusion.Touching the illusion also reveals its false nature.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"resistCheck": "WIS",
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Far",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Invisibility",
|
|
"description": "A creature you touch becomes invisible for the spell's duration.The spell ends if the target attacks or casts a spell.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Judgement",
|
|
"description": "You instantly banish a creature you touch, sending it and all possessions it carries to face the judgment of your god.\n\nYou can banish an intelligent creature of LV 10 or less.\n\nWhen the creature returns in 5 rounds, it has been healed to full hit points if its deeds pleased your god. It has been reduced to 1 hit point if its deeds angered your god. If your god can't judge its actions, it is unchanged.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Knock",
|
|
"description": "A door, window, gate, chest, or portal you can see within range instantly opens, defeating all mundane locks and barriers.This spell creates a loud knock audible to all within earshot.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Lay To Rest",
|
|
"description": "You instantly send an undead creature you touch to its final afterlife, destroying it utterly.\n\nYou can target an undead creature of LV 9 or less.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Levitate",
|
|
"description": "You can float a near distance vertically per round on your turn. You can also push against solid objects to move horizontally.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Light",
|
|
"description": "One object you touch glows with bright, heatless light, illuminating out to a near distance for 1 hour of real time.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"priest",
|
|
"wizard"
|
|
],
|
|
"duration": "1 hour real time",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Lightning Bolt",
|
|
"description": "You shoot a blue-white ray of lightning from your hands, hitting all creatures in a straight line out to a far distance.\n\nCreatures struck by the lightning take 3d6 damage.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Far",
|
|
"damage": "3d6",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Mage Armor",
|
|
"description": "An invisible layer of magical force protects your vitals. Your armor class becomes 14 (18 on a critical spellcasting check) for the spell's duration.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Magic Circle",
|
|
"description": "You conjure a circle of runes out to near-sized cube centered on yourself and name a type of creature (for example, demons).\n\nFor the spell's duration, creatures of the chosen type cannot attack or cast a hostile spell on anyone inside the circle. The chosen creatures also can't possess, compel, or beguile anyone inside the circle.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Magic Missile",
|
|
"description": "You have advantage on your check to cast this spell.\n\nA glowing bolt of force streaks from your open hand, dealing 1d4 damage to one target.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Far",
|
|
"damage": "1d4",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Mass Cure",
|
|
"description": "All allies within near range of you regain 2d6 hit points.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": "2d6"
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Mirror Image",
|
|
"description": "You create a number of illusory duplicates of yourself equal to half your level rounded down (minimum 1). The duplicates surround you and mimic you.\n\nEach time a creature attacks you, the attack misses and causes one of the duplicates to evaporate. If all of the illusions have disappeared, the spell ends.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Misty Step",
|
|
"description": "In a puff of smoke, you teleport a near distance to an area you can see.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Passwall",
|
|
"description": "A tunnel of your height opens in a barrier you touch and lasts for the duration.\n\nThe passage can be up to near distance in length and must be in a straight line.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Pillar Of Salt",
|
|
"description": "A creature you target turns into a statue made of hardened salt.\n\nYou can target a creature you can see of LV 5 or less.If you successfully focus on this spell for 3 rounds in a row, the transformation becomes permanent.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Plane Shift",
|
|
"description": "You fold space and time, transporting yourself and all willing creatures within close range to a location on another plane of your choice.Unless you have been to your intended location before, you appear in a random place on the destination plane.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"priest",
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Polymorph",
|
|
"description": "You transform a creature you touch into another natural creature you choose of equal or smaller size. Any gear the target carries melds into its new form.\n\nThe target gains the creature's physical stats and features, but it retains its non-physical stats and features.\n\nIf the target goes to 0 hit points, it reverts to its true form at half its prior hit points.\n\nYou can target any willing creature with this spell, or an unwilling creature whose level is less than or equal to half your level rounded down (min. 1).",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Power Word Kill",
|
|
"description": "You utter the Word of Doom. One creature you target of LV 9 or less dies if it hears you.\n\nTreat a failed spellcasting check for this spell as a critical failure, and roll the mishap with disadvantage.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Prismatic Orb",
|
|
"description": "You send a strobing orb of energy streaking toward a target within range.\n\nChoose an energy type from fire, cold, or electricity. The orb deals 3d8 damage and delivers a concussive blast of the chosen energy type.If the energy type is anathema to the target's existence (for example, cold energy against a fire elemental), the orb deals double damage to it instead.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Far",
|
|
"damage": "3d8",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Prophecy",
|
|
"description": "You commune directly with your god for guidance.Ask the GM one question.The GM answers the question truthfully using the knowledge your god possesses. Deities are mighty, but not omniscient.\n\nYou cannot cast this spell again until you complete penance.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Protection From Energy",
|
|
"description": "One creature you touch becomes impervious to the wild fury of the elements.Choose fire, cold, or electricity. For the spell's duration, the target is immune to harm from energy of the chosen type.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Protection From Evil",
|
|
"description": "For the spell's duration, chaotic beings have disadvantage on attack rolls and hostile spellcasting checks against the target. These beings also can't possess, compel, or beguile it.\n\nWhen cast on an already-possessed target, the possessing entity makes a CHA check vs. the last spellcasting check. On a failure, the entity is expelled.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"resistCheck": "CHA",
|
|
"classes": [
|
|
"priest",
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Rebuke Unholy",
|
|
"description": "You rebuke creatures who oppose your alignment, forcing them to flee. You must present a holy symbol to cast this spell.\n\nIf you are lawful or neutral, this spell affects demons, devils, and outsiders. If you are chaotic, this spell affects angels and natural creatures of the wild.Affected creatures within near of you must make a CHA check vs. your spellcasting check. If a creature fails by 10+ points and is equal to or less than your level, it is destroyed. Otherwise, on a fail, it flees from you for 5 rounds.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"resistCheck": "CHA",
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Resilient Sphere",
|
|
"description": "You conjure a weightless, glassy sphere around you that extends out to close range.\n\nFor the spell's duration, nothing can pass through or crush the sphere.\n\nYou can roll the sphere a near distance on your turn.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Regenerate",
|
|
"description": "A creature you touch regains 1d4 hit points on your turn for the duration. This spell also regrows lost body parts.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": "1d4"
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Restoration",
|
|
"description": "With the touch of your hands, you expunge curses and illnesses. One curse, illness, or affliction of your choice affecting the target creature ends.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Scrying",
|
|
"description": "You look into a crystal ball or reflecting pool, calling up images of a distant place.\n\nFor the spell's duration, you can see and hear a creature or location you choose that is on the same plane.\n\nThis spell is DC 18 to cast if you try to scry on a creature or location that is unfamiliar to you.\n\nEach round, creatures you view may make a WIS check vs. your last spellcasting check. On a success, they become aware of your magical observation.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"resistCheck": "WIS",
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Shapechange",
|
|
"description": "You transform yourself and any gear you carry into another natural creature you've seen of level 10 or less. You assume the creature's physical stats and features, but you retain your non-physical stats and features (including INT, WIS, and CHA).\n\nIf you go to 0 HP while under the effects of this spell, you revert to your true form at 1 HP.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Sending",
|
|
"description": "You send a brief, mental message to any creature with whom you are familiar who is on the same plane.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Unlimited",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Shield Of Faith",
|
|
"description": "A protective force wrought of your holy conviction surrounds you. You gain a +2 bonus to your armor class for the duration.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Silence",
|
|
"description": "You magically mute sound in a near cube within the spell's range. Creatures inside the area are deafened, and any sounds they create cannot be heard.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Far",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Sleep",
|
|
"description": "You weave a lulling spell that fills a near-sized cube extending from you. Living creatures in the area of effect fall into a deep sleep if they are LV 2 or less.\n\nVigorous shaking or being injured wakes them.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Speak With Dead",
|
|
"description": "A dead body you touch answers your questions in a distant, wheezing voice.\n\nYou can ask the dead body up to three yes or no questions (one at a time). The GM truthfully answers \"yes\" or \"no\" to each.\n\nIf you cast this spell more than once in 24 hours, treat a failed spellcasting check for it as a critical failure instead.",
|
|
"tier": 3,
|
|
"dc": 13,
|
|
"classes": [
|
|
"priest",
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Smite",
|
|
"description": "You call down punishing flames on a creature you can see within range. It takes 1d6 damage.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Near",
|
|
"damage": "1d6",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Stoneskin",
|
|
"description": "Your skin becomes like granite. For the spell's duration, your armor class becomes 17 (20 on a critical spellcasting check).",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Summon Extraplanar",
|
|
"description": "You reach into the outer planes, summoning forth a creature.You summon an elemental or outsider of LV 7 or less. The creature is under your control and acts on your turn.\n\nIf you lose focus on this spell, you lose control of the creature and it becomes hostile toward you and your allies.\n\nYou must pass a spellcasting check on your turn to return the creature to the outer planes.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Telekinesis",
|
|
"description": "You lift a creature or object with your mind. Choose a target that weighs 1,000 pounds or less.\n\nYou can move it a near distance in any direction and hold it in place.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Far",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Teleport",
|
|
"description": "You and any willing creatures you choose within close range teleport to a location you specify on your same plane.\n\nYou can travel to a known teleportation sigil or to a location you've been before. Otherwise, you have a 50% chance of arriving off-target.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Close",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Turn Undead",
|
|
"description": "You rebuke undead creatures, forcing them to flee. You must present a holy symbol to cast this spell.Undead creatures within near of you must make a CHA check vs. your spellcasting check. If a creature fails by 10+ points and is equal to or less than your level, it is destroyed. Otherwise, on a fail, it flees from you for 5 rounds.",
|
|
"tier": 1,
|
|
"dc": 11,
|
|
"resistCheck": "CHA",
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Wall Of Force",
|
|
"description": "You lift your hands, conjuring a transparent wall of force.\n\nThe thin wall must be contiguous and can cover a near-sized area in width and length. You choose its shape.Nothing on the same plane can physically pass through the wall.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Wish",
|
|
"description": "This mighty spell alters reality.\n\nMake a single wish, stating it as exactly as possible. Your wish occurs, as interpreted by the GM.\n\nTreat a failed spellcasting check for this spell as a critical failure, and roll the mishap with disadvantage.",
|
|
"tier": 5,
|
|
"dc": 15,
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "Instant",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Web",
|
|
"description": "You create a near-sized cube of sticky, dense spider web within the spell's range. A creature stuck in the web can't move and must succeed on a STR check vs. your spellcasting check to free itself.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"resistCheck": "STR",
|
|
"classes": [
|
|
"wizard"
|
|
],
|
|
"duration": "5 rounds",
|
|
"range": "Far",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Wrath",
|
|
"description": "Your weapons become magical +2 and deal an additional d8 damage for the spell's duration.",
|
|
"tier": 4,
|
|
"dc": 14,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "10 rounds",
|
|
"range": "Self",
|
|
"damage": "",
|
|
"healing": ""
|
|
},
|
|
{
|
|
"source": "core rules",
|
|
"name": "Zone Of Truth",
|
|
"description": "You compel a creature you can see to speak truth. It can't utter a deliberate lie while within range.",
|
|
"tier": 2,
|
|
"dc": 12,
|
|
"classes": [
|
|
"priest"
|
|
],
|
|
"duration": "Focus",
|
|
"range": "Near",
|
|
"damage": "",
|
|
"healing": ""
|
|
}
|
|
]
|
|
};
|
|
</script>
|
|
</div>
|