mirror of
https://github.com/Nighthawk42/roll20-character-sheets.git
synced 2026-09-02 13:12:31 +00:00
1920 lines
82 KiB
HTML
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 & 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>
|
|
|
|
|
|
|
|
|