Files
roll20-character-sheets/Shadowdark-Unofficial/Shadowdark-Unofficial.html
T
2024-06-03 13:15:14 -04:00

1920 lines
82 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>
</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</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</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 &amp; Spells</h1>
<div>
<fieldset class="repeating_attack">
<div class="sheet-fieldset-item">
<div class="sheet-checkbox-cog" title="Toggle 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">
<div class="sheet-form-group">
<label>Edit Name:</label>
<input type="text" name="attr_atkname" placeholder="Weapon or Spell">
</div>
<div class="sheet-form-group">
<label class="sheet-form-checkbox">
<span>Roll Stat + Bonus:</span>
</label>
</div>
<div class="sheet-form-group">
<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>
<span>+</span>
<input type="text" name="attr_atkmod">
</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, 1 turn, 1 minute, etc.">
</div>
<div class="sd-col">
<label>DC to Cast:</label>
<input type="text" name="attr_castdc">
</div>
</div>
</div>
<div class="sheet-form-group">
<label>Attack/Spell Range:</label>
<input type="text" name="attr_atkrange" placeholder="close, far, near, etc.">
</div>
<div class="sheet-form-group">
<label>Other Bonus:</label>
<input type="text" name="attr_atkmagic">
<label>Minimum Die Roll To Crit:</label>
<input type="text" name="attr_atkcritrange" value="20">
</div>
<div class="sheet-form-group">
<label class="sheet-form-checkbox">
<input type="checkbox" name="attr_dmgflag" value="{{damage=1}} {{dmg1flag=1}}" checked>
<span>Damage Type 1</span>
</label>
</div>
<div class="sheet-form-group">
<label>Damage Roll + Bonus:</label>
<input type="text" name="attr_dmgbase">
<span>+</span>
<input type="text" name="attr_dmgmod">
</div>
<div class="sheet-form-group">
<label>Crit Roll or Damage (if not roll twice):</label>
<input type="text" name="attr_dmgcustcrit">
</div>
<div class="sheet-form-group">
<label class="sheet-form-checkbox">
<input type="checkbox" name="attr_dmg2flag" value="{{damage=1}} {{dmg2flag=1}}">
<span>Damage Type 2</span>
</label>
</div>
<div class="sheet-form-group">
<label>Damage Roll + Bonus:</label>
<input type="text" name="attr_dmg2base">
<span>+</span>
<input type="text" name="attr_dmg2mod">
</div>
<div class="sheet-form-group">
<label>Crit Roll or Damage (if not roll twice):</label>
<input type="text" name="attr_dmg2custcrit">
</div>
<div class="sheet-form-group sheet-form-group-column">
<label>Description or Flavor Text:</label>
<textarea name="attr_atk_desc"></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" checked="checked" title="Equipped / Usable">
<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">
<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="Current" 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="Toggle 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">
<div class="sheet-form-group">
<label>
Edit Trait<br>
<input type="text" name="attr_name" class="trait-name" maxlength="60">
</label>
</div>
<div class="sheet-form-group">
<label>
Edit Details<br>
<textarea name="attr_description" class="trait-desc"></textarea>
</label>
</div>
<div class="sheet-form-group">
<label class="sheet-form-checkbox">
<input type="checkbox" name="attr_hide_details" value="1">
<span>Hide Details 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">
<label class="sheet-form-checkbox" title="@{show_options}">
<input type="checkbox" name="attr_show_options" value="1">
<span>Show Sheet Options</span>
</label>
</div>
<input class="sheet-toggle-options" name="attr_show_options" type="hidden" value="0">
<div class="sheet-section sheet-section-options">
<h1>Sheet Options</h1>
<div class="sheet-form-body">
<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">
<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>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}">
</div>
<div class="sheet-form-group">
<div>
<button type="action" name="act_importjson">Import Shadowdarklings JSON</button>
<button type="action" name="act_importmonster">Import Monster Text from Book</button>
</div>
<label class="sheet-form-checkbox" title="@{sheetwipe_confirm_flag}">
<input type="checkbox" name="attr_sheetwipe_confirm_flag" value="1">
<span>Confirm Import: "By checking this box, I officially swear that I want to replace my stats"</span>
</label>
</div>
<div class="sheet-form-group">
<label>JSON (Shadowdarklings format) or Monster Text (Core rules PDF format)</label>
<textarea name="attr_json" title="@{json}"></textarea>
</div>
<div class="sheet-form-group">
<label>Version:</label>
<input type="text" name="attr_version" value="2024.2.29.0" hidden disabled>
<span name="attr_version"></span>
</div>
<div>
<span>Roll20 Character Sheet for Shadowdark RPG</span>
<div class="legal">
<div>LEGAL:</div>
<div>
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.
</div>
<div>
Have fun, and use at your own risk!
</div>
<div>CREDIT AND THANKS:</div>
<div>
<ul class="sd-credits">
<li>Giffyglyph for creating the 5e Darker Dungeons sheet because I copied much the script into this one.</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 letting for keeping my friends and me (roll20: CyberSasquatch; discord: thescratch) sane
during the pandemic</li>
<li>Last but not least: THANK YOU for using this (and for your patience while bugs are worked out)
</li>
</ul>
</div>
</p>
</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">
{{#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-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
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.max(!isNpc ? 4 : 99, Math.floor(((sheet.stats?.STR || 10) - 10) / 2)),
dexterity_mod: sheet.stats?.dex_mod !== undefined ? sheet.stats.dex_mod : Math.max(!isNpc ? 4 : 99, Math.floor(((sheet.stats?.DEX || 10) - 10) / 2)),
constitution_mod: sheet.stats?.con_mod !== undefined ? sheet.stats.con_mod : Math.max(!isNpc ? 4 : 99, Math.floor(((sheet.stats?.CON || 10) - 10) / 2)),
intelligence_mod: sheet.stats?.int_mod !== undefined ? sheet.stats.int_mod : Math.max(!isNpc ? 4 : 99, Math.floor(((sheet.stats?.INT || 10) - 10) / 2)),
wisdom_mod: sheet.stats?.wis_mod !== undefined ? sheet.stats.wis_mod : Math.max(!isNpc ? 4 : 99, Math.floor(((sheet.stats?.WIS || 10) - 10) / 2)),
charisma_mod: sheet.stats?.cha_mod !== undefined ? sheet.stats.cha_mod : Math.max(!isNpc ? 4 : 99, 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;
try {
// add languages
if (sheet.languages?.length) {
create_trait_from_feature({name: "Languages Known", description: sheet.languages });
}
// add spells
if (sheet.spellsKnown?.length && sheet.spellsKnown !== "None") {
create_trait_from_feature({name: "Spells Known", description: sheet.spellsKnown });
}
// add ledger - hide details to avoid scrolling
if (sheet.ledger?.length) {
let hideDetails = true;
create_trait_from_feature({name: "Ledger",
description: sheet.ledger
.map(o=> {
let str = [];
str.push(`${o.desc}:`);
if(o.goldChange !== 0) {
let sign = o.goldChange < 0 ? "" : "+";
str.push(`${sign}${o.goldChange} GP`);
}
if(o.silverChange !== 0) {
let sign = o.silverChange < 0 ? "" : "+";
str.push(`${sign}${o.silverChange} SP`);
}
if(o.copperChange !== 0) {
let sign = o.copperChange < 0 ? "" : "+";
str.push(`${sign}${o.copperChange} CP`);
}
if(o.notes?.length) {
str.push((`(${o.notes})`));
}
return str.join(" ");
})
.join("\n")
}, hideDetails);
}
// add bonuses (talents only)
if (sheet.bonuses) {
let hideDetails = false;
sheet.bonuses
.filter(o=>o.sourceCategory === "Talent")
.forEach(o=>{
create_trait_from_feature({
name: `Talent: ${o.bonusName} (${addSpaces(o.name)})`,
description: `(level ${o.gainedAtLevel}) ${addSpaces(o.name)}: ${o.bonusName}`
}, hideDetails);
});
}
}
catch (ex) {
console.log(`Failed to import features\n${ex}`);
}
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) {
// help GMs: add attackText as a Multiattack 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: "Multiattack", 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}`); }
});
}
monster.actions.forEach(o=> {
try { create_trait_from_feature(o); }
catch(ex) { console.log(`Failed to add attack ${o}\n${ex}`); }
});
}
let addSpaces = function(str) {
return str.replace(/([a-z])([A-Z])/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: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;
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", "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 + " Magic Bonus" : "";
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}";
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`] = "0"; // monster attacks use mod only, not stats
update[`${prefix}_atkmod`] = action.mod === undefined ? "0" : 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}_atkmagic`] = action.magBonus || "";
update[`${prefix}_atkcritrange`] = action.critRange || "20";
update[`${prefix}_dmgflag`] = action.damage === undefined ? "" : "{{damage=1}} {{dmg1flag=1}}";
update[`${prefix}_dmgbase`] = action.damage || "";
update[`${prefix}_dmgmod`] = action.damageMod || "";
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 ? "Effect: " + 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
});
});
};
</script>
</div>