diff --git a/.github/workflows/build-sheets.yml b/.github/workflows/build-sheets.yml new file mode 100644 index 0000000..2220f5d --- /dev/null +++ b/.github/workflows/build-sheets.yml @@ -0,0 +1,43 @@ +name: Build Sheets + +on: + workflow_dispatch: + push: + paths: + - 'templates/**' + - 'components/**' + - 'scripts/build_sheets.py' + - '.github/workflows/build-sheets.yml' + +permissions: + contents: write + +jobs: + build-sheets: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Build sheets from templates + run: python scripts/build_sheets.py + + - name: Commit built sheets if changed + run: | + if git diff --quiet -- OutputSheets; then + echo "No OutputSheets changes detected" + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add OutputSheets + git commit -m "chore: rebuild OutputSheets from templates" + git push diff --git a/OutputSheets/d20/fantasy/xmlhtml/csheet_known_spells.htm.ftl b/OutputSheets/d20/fantasy/xmlhtml/csheet_known_spells.htm.ftl index 4c62cf8..dd28777 100644 --- a/OutputSheets/d20/fantasy/xmlhtml/csheet_known_spells.htm.ftl +++ b/OutputSheets/d20/fantasy/xmlhtml/csheet_known_spells.htm.ftl @@ -129,7 +129,6 @@ -

${pcstring('NAME')}

@@ -454,6 +453,7 @@
+ @@ -551,10 +551,22 @@ <#assign charSize = pcstring('SIZE')?lower_case /> +<#assign sizeKey = charSize /> +<#if charSize?starts_with("tiny")><#assign sizeKey = "t" /> +<#elseif charSize?starts_with("small")><#assign sizeKey = "s" /> +<#elseif charSize?starts_with("medium")><#assign sizeKey = "m" /> +<#elseif charSize?starts_with("large")><#assign sizeKey = "l" /> +<#elseif charSize?starts_with("huge")><#assign sizeKey = "h" /> +<#elseif charSize?starts_with("gargantuan")><#assign sizeKey = "g" /> +<#elseif charSize?starts_with("colossal")><#assign sizeKey = "c" /> + <#assign unarmedDie = "1d3" /> -<#if charSize = "s"><#assign unarmedDie = "1d2" /> -<#elseif charSize = "m"><#assign unarmedDie = "1d3" /> -<#elseif charSize = "l"><#assign unarmedDie = "1d4" /> +<#if sizeKey == "t" || sizeKey == "s"><#assign unarmedDie = "1d2" /> +<#elseif sizeKey == "m"><#assign unarmedDie = "1d3" /> +<#elseif sizeKey == "l"><#assign unarmedDie = "1d4" /> +<#elseif sizeKey == "h"><#assign unarmedDie = "1d6" /> +<#elseif sizeKey == "g"><#assign unarmedDie = "1d8" /> +<#elseif sizeKey == "c"><#assign unarmedDie = "2d6" /> <#assign strMod = pcstring('STAT.0.MOD.SIGN') /> @@ -723,7 +735,7 @@ <#if (spelllevelcount > 0)>
- <#if (level = 0)>Cantrips (Level 0) — Unlimited Uses<#else>Level ${level} — Spells/Day: ${pcstring('SPELLLISTCAST.${class}.${level}')} + <#if (level == 0)>Cantrips (Level 0) — Unlimited Uses<#else>Level ${level} — Spells/Day: ${pcstring('SPELLLISTCAST.${class}.${level}')}
@@ -749,7 +761,7 @@ [${pcstring('SPELLMEM.${class}.0.${level}.${spell}.SOURCE')}]
- <#if (level = 0)>∞<#else>${pcstring('SPELLLISTCAST.${class}.${level}')} + <#if (level == 0)>∞<#else>${pcstring('SPELLLISTCAST.${class}.${level}')} <#if !hasNoSave>${spSaveShort}
DC ${spDC}
@@ -786,7 +798,7 @@ <#if (spelllevelcount > 0)>
- <#if (level = 0)>Cantrips (Level 0) — Unlimited Uses<#else>Level ${level} — Prepared: ${pcstring('SPELLLISTCAST.${class}.${level}')} + <#if (level == 0)>Cantrips (Level 0) — Unlimited Uses<#else>Level ${level} — Prepared: ${pcstring('SPELLLISTCAST.${class}.${level}')}
@@ -812,7 +824,7 @@ [${pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.SOURCE')}] @@ -706,7 +718,6 @@ -

Prepared Spells

Spell descriptions shown here come from PCGen's built-in effect summary. For the full text of any spell, refer to the source book listed in brackets — or search the spell name on the Archives of Nethys (aonprd.com) for free online access to the complete Pathfinder rules text. @@ -722,7 +733,7 @@ <#if (spelllevelcount > 0)>
- <#if (level = 0)>Cantrips (Level 0) — Unlimited Uses<#else>Level ${level} — Prepared: ${pcstring('SPELLLISTCAST.${class}.${level}')} + <#if (level == 0)>Cantrips (Level 0) — Unlimited Uses<#else>Level ${level} — Prepared: ${pcstring('SPELLLISTCAST.${class}.${level}')}
- <#if (level = 0)>∞<#else><@loop from=1 to=pcvar("SPELLMEM.${class}.${spellbook}.${level}.${spell}.TIMES")>☐ + <#if (level == 0)>∞<#else><@loop from=1 to=pcvar("SPELLMEM.${class}.${spellbook}.${level}.${spell}.TIMES")>☐ <#if !hasNoSave>${spSaveShort}
DC ${spDC}
@@ -899,7 +911,6 @@ -

Equipment

@@ -1336,5 +1347,6 @@ Player: ${pcstring('PLAYERNAME')} — Character: ${pcstring('NAME')}
+ diff --git a/OutputSheets/d20/fantasy/xmlhtml/csheet_prepared_spells.htm.ftl b/OutputSheets/d20/fantasy/xmlhtml/csheet_prepared_spells.htm.ftl index 522b9db..5da4c94 100644 --- a/OutputSheets/d20/fantasy/xmlhtml/csheet_prepared_spells.htm.ftl +++ b/OutputSheets/d20/fantasy/xmlhtml/csheet_prepared_spells.htm.ftl @@ -129,7 +129,6 @@ -

${pcstring('NAME')}

@@ -454,6 +453,7 @@
+ @@ -551,10 +551,22 @@ <#assign charSize = pcstring('SIZE')?lower_case /> +<#assign sizeKey = charSize /> +<#if charSize?starts_with("tiny")><#assign sizeKey = "t" /> +<#elseif charSize?starts_with("small")><#assign sizeKey = "s" /> +<#elseif charSize?starts_with("medium")><#assign sizeKey = "m" /> +<#elseif charSize?starts_with("large")><#assign sizeKey = "l" /> +<#elseif charSize?starts_with("huge")><#assign sizeKey = "h" /> +<#elseif charSize?starts_with("gargantuan")><#assign sizeKey = "g" /> +<#elseif charSize?starts_with("colossal")><#assign sizeKey = "c" /> + <#assign unarmedDie = "1d3" /> -<#if charSize = "s"><#assign unarmedDie = "1d2" /> -<#elseif charSize = "m"><#assign unarmedDie = "1d3" /> -<#elseif charSize = "l"><#assign unarmedDie = "1d4" /> +<#if sizeKey == "t" || sizeKey == "s"><#assign unarmedDie = "1d2" /> +<#elseif sizeKey == "m"><#assign unarmedDie = "1d3" /> +<#elseif sizeKey == "l"><#assign unarmedDie = "1d4" /> +<#elseif sizeKey == "h"><#assign unarmedDie = "1d6" /> +<#elseif sizeKey == "g"><#assign unarmedDie = "1d8" /> +<#elseif sizeKey == "c"><#assign unarmedDie = "2d6" /> <#assign strMod = pcstring('STAT.0.MOD.SIGN') />
@@ -748,7 +759,7 @@ [${pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.SOURCE')}]
- <#if (level = 0)>∞<#else><@loop from=1 to=pcvar("SPELLMEM.${class}.${spellbook}.${level}.${spell}.TIMES")>☐ + <#if (level == 0)>∞<#else><@loop from=1 to=pcvar("SPELLMEM.${class}.${spellbook}.${level}.${spell}.TIMES")>☐ <#if !hasNoSave>${spSaveShort}
DC ${spDC}
@@ -835,7 +846,6 @@ -

Equipment

@@ -1272,5 +1282,6 @@ Player: ${pcstring('PLAYERNAME')} — Character: ${pcstring('NAME')}
+ diff --git a/README.md b/README.md index 4faa8cf..7526ac4 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,30 @@ Custom PCGen output sheets for Pathfinder 1e. Currently includes a prepared-spel - A collection of PCGen **output sheets** (FreeMarker `.ftl` templates) for Pathfinder 1e characters. - The sheets are drop-in replacements or additions for the standard PCGen output-sheet folder. -- `OutputSheets/d20/fantasy/xmlhtml/csheet_prepared_spells.htm.ftl` — a character sheet focused on prepared spells, styled for easy table-side reading. +- `templates/d20/fantasy/xmlhtml/` — source sheet templates using component placeholders. +- `components/` — reusable sheet blocks (skills, feats, weapons, inventory, spellbook, prepared spells, quick view, common conditions, biography, and more). +- `OutputSheets/d20/fantasy/xmlhtml/` — compiled output sheets ready to copy into PCGen. -If you want to change how the sheet looks or what it shows, edit the `.ftl` file directly. The template uses PCGen's `${pcstring(...)}` and `<#...>` directives to pull character data at export time. +If you want to change how the sheet looks or what it shows, edit files in `templates/` and `components/`, then compile to `OutputSheets/`. The templates use PCGen's `${pcstring(...)}` and `<#...>` directives to pull character data at export time. + + +## Component syntax + +Templates use component tokens in this format (templates are now mostly style + ordered component tokens): + +``` +{{ component:skills }} +``` + +During build, each token is replaced with the content of `components/.ftl`. + +## Build compiled sheets + +``` +python scripts/build_sheets.py +``` + +This compiles everything from `templates/` to `OutputSheets/` using `components/`. ## Where to put the sheets @@ -36,7 +57,11 @@ PCGen looks for output sheets inside its own `outputsheets` folder. The director ## Where the important pieces live -- `OutputSheets/d20/fantasy/xmlhtml/csheet_prepared_spells.htm.ftl` — the prepared-spells sheet template +- `templates/d20/fantasy/xmlhtml/csheet_prepared_spells.htm.ftl` — prepared-spells source template +- `templates/d20/fantasy/xmlhtml/csheet_known_spells.htm.ftl` — known+prepared spells source template +- `components/*.ftl` — reusable sheet blocks inserted during build (including quick view, common conditions, biography, rules references, and combat sections) +- `scripts/build_sheets.py` — local compiler for templates/components into `OutputSheets/` +- `.github/workflows/build-sheets.yml` — CI workflow that rebuilds and auto-commits `OutputSheets/` ## Contributing @@ -50,9 +75,10 @@ Pull requests are welcome, especially if you want to: ### A good contribution path 1. Fork the repository or create a branch. -2. Make your changes to the `.ftl` template(s). -3. Export a test character from PCGen to verify the output looks correct. -4. Open a pull request with a clear explanation of what the sheet shows or how the layout changed. +2. Make your changes in `templates/` and `components/`. +3. Compile with `python scripts/build_sheets.py` to regenerate `OutputSheets/`. +4. Export a test character from PCGen to verify the output looks correct. +5. Open a pull request with a clear explanation of what the sheet shows or how the layout changed. ### Content conventions diff --git a/components/ability_influence.ftl b/components/ability_influence.ftl new file mode 100644 index 0000000..0322a04 --- /dev/null +++ b/components/ability_influence.ftl @@ -0,0 +1,51 @@ + +
+

Ability Influence (Quick Calc)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AbilityScore / ModPrimary Effects On This SheetCurrent Derived Values
STR14 (+2)Melee attack, melee damage, CMB, CMD, carry/lift limitsMelee: +2 | CMB: +2 | CMD: 13
DEX12 (+1)Ranged attack, initiative, AC ability bonus, CMD, Dexterity skills, Reflex saveRanged: +1 | Init: +1 | AC ability: 1
CON14 (+2)Hit points per level/HD, Fortitude save, concentration-related checksMax HP: 10 | Hit Dice: (1d8)+2
INT11 (+0)Bonus skill ranks/level, INT-based skills, knowledge checks, some feat prerequisitesSee Skills table for INT-based totals
WIS16 (+3)Will save, WIS-based skills, divine spellcasting checks/DCs when applicableSee Saves/Skills and Prepared Spells sections
CHA12 (+1)Social skills, class features that key from CHA, turning/channel effects when usedSee class feature and ability notes
+
+ Ability damage/drain quick rule: every 2 points usually changes the ability modifier by 1, which then shifts all dependent values above. +
+
diff --git a/components/ability_scores.ftl b/components/ability_scores.ftl new file mode 100644 index 0000000..35b5e67 --- /dev/null +++ b/components/ability_scores.ftl @@ -0,0 +1,13 @@ + +
+

Ability Scores

+
+<@loop from=0 to=pcvar('COUNT[STATS]-1') ; stat , stat_has_next> +
+ ${pcstring('STAT.${stat}.NAME')} + ${pcstring('STAT.${stat}.NOTEMP.NOEQUIP')} + ${pcstring('STAT.${stat}.MOD.NOTEMP.NOEQUIP')} +
+ +
+
diff --git a/components/aoo_reference.ftl b/components/aoo_reference.ftl new file mode 100644 index 0000000..e2c5226 --- /dev/null +++ b/components/aoo_reference.ftl @@ -0,0 +1,50 @@ + +
+

AoO Quick Reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ActionNotes
Move out of threatened square5-ft step, withdraw (first square), or Acrobatics can avoid
Ranged attack in meleeAny ranged attack while threatened
Cast spell in meleeCast defensively to avoid provoking (Concentration check)
Drink potion / use scrollUsing items in melee commonly provokes
Stand up from proneCommon trigger after trip
Combat maneuver (without Improved feat)Trip, disarm, grapple, etc.; improved feat usually prevents
Retrieve stowed itemDigging in backpack/pouch while threatened
Pick up itemPicking up from ground in melee
Load crossbowMost loading actions in melee provoke
Unarmed strike without Improved Unarmed StrikeBarehanded attacks vs armed foes can provoke
+
diff --git a/components/armor_shields.ftl b/components/armor_shields.ftl new file mode 100644 index 0000000..cadeeb2 --- /dev/null +++ b/components/armor_shields.ftl @@ -0,0 +1,35 @@ +
+

Armor & Shields

+
+ + + + + + + + + + +<@loop from=0 to=2 ; ar , ar_has_next> +<#if (pcstring("ARMOR.${ar}.NAME") != "")> + + + + + + + + + + + +
ItemTypeAC BonusMax Dex*Check Pen†Spell Fail
${pcstring('ARMOR.EQUIPPED.${ar}.NAME')}${pcstring('ARMOR.EQUIPPED.${ar}.TYPE')}${pcstring('ARMOR.EQUIPPED.${ar}.TOTALAC')}${pcstring('ARMOR.EQUIPPED.${ar}.MAXDEX')}${pcstring('ARMOR.EQUIPPED.${ar}.ACCHECK')}${pcstring('ARMOR.EQUIPPED.${ar}.SPELLFAIL')}
+
+ * Max Dex: Dex bonus to AC cannot exceed this — + † Check Penalty: applies to Str/Dex skill checks and attacks if not proficient — + Spell Failure: % chance arcane spells fail (divine spells ignore this) — + Heavy armor reduces speed to 20ft — sleeping in medium/heavy armor leaves you fatigued +
+
+
diff --git a/components/biography.ftl b/components/biography.ftl new file mode 100644 index 0000000..709f4cf --- /dev/null +++ b/components/biography.ftl @@ -0,0 +1,12 @@ +
+

Biography

+
+ <#assign bioRaw = pcstring('BIO') /> + <#assign bioRaw = bioRaw?replace("<","<") /> + <#assign bioRaw = bioRaw?replace(">",">") /> + <#assign bioRaw = bioRaw?replace("&","&") /> + <#assign bioRaw = bioRaw?replace(""",'"') /> + <#assign bioRaw = bioRaw?replace("'","'") /> + ${bioRaw} +
+
diff --git a/components/combat.ftl b/components/combat.ftl new file mode 100644 index 0000000..c69f639 --- /dev/null +++ b/components/combat.ftl @@ -0,0 +1,69 @@ + +

Combat

+
+ + + + + + + + + +
Max HP${pcstring('HP')}
HP Lost Today 
Current HP 
Initiative${pcstring('INITIATIVEMOD')}
Speed<@loop from=0 to=pcvar('COUNT[MOVE]-1') ; mv , mv_has_next>${pcstring('MOVE.${mv}.RATE')}
DR<#if (pcstring('DR') != '')>${pcstring('DR')}<#else>0
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ AC + ${pcstring('AC.Total')} + :Touch${pcstring('AC.Touch')}:Flat${pcstring('AC.Flatfooted')}=Base${pcstring('AC.Base')}+Armor*${pcstring('AC.Armor')}+Shield${pcstring('AC.Shield')}+Dex${pcstring('AC.Ability')}+Size${pcstring('AC.Size')}+Nat${pcstring('AC.NaturalArmor')}+Dodge${pcstring('AC.Dodge')}+Defl${pcstring('AC.Deflection')}+Misc${pcstring('AC.Misc')}
+
+ Touch AC ignores Armor, Shield, and Natural Armor — + Flat-Footed AC ignores Dexterity and Dodge — + Deflection and Size apply to all three — + * Armor bonus reduced by check penalty if not proficient — + Max Dex cap limits Dex bonus when wearing armor +
+ + + + + + + + + + +
BAB${pcstring('ATTACK.MELEE')}
Melee Hit${pcstring('ATTACK.MELEE.TOTAL')}
Ranged Hit${pcstring('ATTACK.RANGED.TOTAL')}
Hit Die${pcstring('HITDICE')}
CMB${pcstring('VAR.CMB.INTVAL.SIGN')}
CMD${pcstring('VAR.CMD.INTVAL')}
SR<#if (pcstring('SR') != '')>${pcstring('SR')}<#else>0
+
+ CMB = BAB + Str + Size — CMD = 10 + BAB + Str + Dex + Size — + SR: attacker rolls d20 + caster level ≥ SR to affect you +
+
diff --git a/components/combat_conditionals.ftl b/components/combat_conditionals.ftl new file mode 100644 index 0000000..e6340d1 --- /dev/null +++ b/components/combat_conditionals.ftl @@ -0,0 +1,9 @@ +
+ Conditional Attack / Combat Modifiers:
+ <#assign hasCombatCond = false /> + <@loop from=0 to=pcvar('countdistinct("ABILITIES","ASPECT=CombatBonus")-1') ; ab , ab_has_next> + <#assign hasCombatCond = true /> + • ${pcstring('ABILITYALL.ANY.${ab}.ASPECT=CombatBonus.ASPECT.CombatBonus')}
+ + <#if !hasCombatCond>No modifiers +
diff --git a/components/combat_maneuvers.ftl b/components/combat_maneuvers.ftl new file mode 100644 index 0000000..404bb34 --- /dev/null +++ b/components/combat_maneuvers.ftl @@ -0,0 +1,71 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ManeuverCMBCMDNotes
Grapple${pcstring('VAR.CMB_Grapple.INTVAL.SIGN')}${pcstring('VAR.CMD_Grapple.INTVAL')}Pin, tie up, damage, or move a grappled foe. Grappled = −2 attack/AC, no two-handed.
Trip${pcstring('VAR.CMB_Trip.INTVAL.SIGN')}<#if (pcvar("CantBeTripped") != 0)>Immune<#else>${pcstring('VAR.CMD_Trip.INTVAL')}Knock prone. Prone = −4 melee attack, −4 AC vs melee, +4 AC vs ranged.
Disarm${pcstring('VAR.CMB_Disarm.INTVAL.SIGN')}${pcstring('VAR.CMD_Disarm.INTVAL')}Knock weapon from foe. Beat CMD by 10+ = item lands 10ft away.
Bull Rush${pcstring('VAR.CMB_BullRush.INTVAL.SIGN')}${pcstring('VAR.CMD_BullRush.INTVAL')}Push foe back 5ft + 5ft per 5 over CMD. You may follow.
Sunder${pcstring('VAR.CMB_Sunder.INTVAL.SIGN')}${pcstring('VAR.CMD_Sunder.INTVAL')}Damage a held/worn item. Broken = −2 attack/damage or halved effectiveness.
Overrun${pcstring('VAR.CMB_Overrun.INTVAL.SIGN')}${pcstring('VAR.CMD_Overrun.INTVAL')}Move through foe's space. Fail = blocked; beat by 5+ = foe prone.
Dirty Trick${pcstring('VAR.CMB_DirtyTrick.INTVAL.SIGN')}${pcstring('VAR.CMD_DirtyTrick.INTVAL')}Blind, entangle, or sicken 1 round (+1 per 5 over CMD). Std action to remove.
Drag${pcstring('VAR.CMB_Drag.INTVAL.SIGN')}${pcstring('VAR.CMD_Drag.INTVAL')}Pull foe 5ft + 5ft per 5 over CMD toward you. Must move with them.
Reposition${pcstring('VAR.CMB_Reposition.INTVAL.SIGN')}${pcstring('VAR.CMD_Reposition.INTVAL')}Move foe to any adjacent square. Foe must remain adjacent.
Steal${pcstring('VAR.CMB_Steal.INTVAL.SIGN')}${pcstring('VAR.CMD_Steal.INTVAL')}Take one carried/worn item (not wielded). No free hand required.
+
All maneuvers provoke AoO unless you have the Improved feat for that maneuver — failing by 5+ lets the foe attempt the same maneuver on you as a free action
+
diff --git a/components/common_conditions.ftl b/components/common_conditions.ftl new file mode 100644 index 0000000..95444b4 --- /dev/null +++ b/components/common_conditions.ftl @@ -0,0 +1,119 @@ + +
+

Common Conditions & Modifiers

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Condition / SituationMain EffectQuick Modifier
ShakenGeneral fear penalties−2 attacks, saves, skills, ability checks
FrightenedAs shaken; must flee if possible−2 attacks, saves, skills, checks
CourageMorale combat boost+1 to hit; +1 saves vs fear
GuidanceSingle-use bonus+1 attack, +1 save, or +1 skill check
SickenedNausea/weakness penalties−2 attacks, weapon dmg, saves, skills, checks
FatiguedTired; cannot run/charge−2 Str, −2 Dex
ExhaustedSevere fatigue; slower movement−6 Str, −6 Dex
ProneWorse in melee, better vs ranged−4 melee attacks, −4 AC vs melee, +4 AC vs ranged
Flat-FootedNot ready to reactLose Dex to AC (and dodge bonuses)
EntangledRestricted movement/offense−2 attack, −4 Dex, move at half speed
GrappledLimited actions in grapple−2 attacks and AC; no AoO
PinnedImmobile in grappleCannot move; very limited actions
StaggeredOnly one major actionEither one standard or one move action
StunnedDrop items; no actionsDrop everything, −2 AC, lose Dex to AC, no actions
FlankingAttacking same foe from opposite sides+2 melee attack
ChargeMove then strike aggressively+2 attack, −2 AC until next turn
Fighting DefensivelyTrade attack for defense−4 attack, +2 AC
Total DefenseNo attacks; full defense+4 AC
BlindedCannot see−2 AC, lose Dex to AC, −4 many Str/Dex checks, move half speed
Invisible AttackerDefender cannot see attackerAttacker often +2 melee; target loses Dex to AC; may require miss chance
Helpless / Coup de GraceHelpless targets are vulnerableCoup de grace: auto crit; target Fort save (DC 10 + damage dealt) or die
+
+ Quick reference only: if an effect conflicts with a specific spell, feat, or monster ability, use the specific rule text first. +
+
diff --git a/components/concentration_reference.ftl b/components/concentration_reference.ftl new file mode 100644 index 0000000..945699a --- /dev/null +++ b/components/concentration_reference.ftl @@ -0,0 +1,64 @@ +
+

Concentration Quick Reference

+
+ Concentration Check
+ d20 + caster level + spellcasting ability modifier + other bonuses
+ Spellcasting ability modifier is INT (wizard), WIS (cleric/druid), CHA (sorcerer/bard/oracle), etc. + <#assign hasConcClass = false /> + <@loop from=pcvar('COUNT[SPELLRACE]') to=pcvar('COUNT[SPELLRACE]+COUNT[CLASSES]-1') ; class , class_has_next> + <#if (pcstring("SPELLLISTCLASS.${class}") != '' && pcstring("SPELLLISTCLASS.${class}.CONCENTRATION") != '')> + <#assign hasConcClass = true /> +
${pcstring('SPELLLISTCLASS.${class}')}: d20${pcstring('SPELLLISTCLASS.${class}.CONCENTRATION')} total + (CL ${pcstring('SPELLLISTCLASS.${class}.CASTERLEVEL')} + ${pcstring('SPELLLISTDCSTAT.${class}.0')}) + + + <#if !hasConcClass>
No spellcasting class concentration values found. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
When A Check Is RequiredConcentration DC
Cast defensively (to avoid provoking)15 + (2 × spell level)
Take damage while casting10 + damage dealt + spell level
Taking continuous damage while casting10 + half last damage dealt + spell level
Vigorous motion (mount, rough vehicle, choppy water)10 + spell level
Violent motion (violent weather, heavy turbulence)15 + spell level
Extra violent motion (earthquake-level disruption)20 + spell level
Weather with high wind, rain, or debris5 + spell level
Entangled while casting15 + spell level
Grappled or pinned while casting10 + grappler CMB + spell level
+
+ Common Modifiers: Combat Casting gives +4 on concentration checks to cast defensively or while grappled/pinned — + ability score increases, feats, traits, class features, and situational bonuses also apply.
+ If you fail the concentration check, the spell is lost and has no effect. +
+
diff --git a/components/feats.ftl b/components/feats.ftl new file mode 100644 index 0000000..d5fa38b --- /dev/null +++ b/components/feats.ftl @@ -0,0 +1,55 @@ + +
+
+

Feats

+
+ +<@loop from=0 to=pcvar('countdistinct("ABILITIES","CATEGORY=FEAT","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")-1') ; ft , ft_has_next> + + + + +
+ ${pcstring('ABILITYALL.Feat.VISIBLE.${ft}')}
+ [${pcstring('ABILITYALL.Feat.VISIBLE.${ft}.SOURCE')}]
+ ${pcstring('ABILITYALL.Feat.VISIBLE.${ft}.BENEFIT')} +
+
+
+ +<#if (pcvar('count("ABILITIES","CATEGORY=Special Ability","TYPE=Trait","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")') > 0)> +
+

Traits

+
+ +<@loop from=0 to=pcvar('count("ABILITIES","CATEGORY=Special Ability","TYPE=Trait","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")-1') ; tr , tr_has_next> + + + + +
+ ${pcstring('ABILITYALL.Special Ability.VISIBLE.${tr}.TYPE=Trait')}
+ [${pcstring('ABILITYALL.Special Ability.VISIBLE.${tr}.TYPE=Trait.SOURCE')}]
+ ${pcstring('ABILITYALL.Special Ability.VISIBLE.${tr}.TYPE=Trait.DESC')} +
+
+
+ + +<#if (pcstring('DOMAIN.1') != '')> +
+

Domains

+
+ +<@loop from=1 to=pcvar('COUNT[DOMAINS]') ; dm , dm_has_next> + + + + +
+ ${pcstring('DOMAIN.${dm}')}
+ ${pcstring('DOMAIN.${dm}.POWER')} +
+
+
+ diff --git a/components/footer.ftl b/components/footer.ftl new file mode 100644 index 0000000..d36cdf8 --- /dev/null +++ b/components/footer.ftl @@ -0,0 +1,5 @@ +
+
+ PCGen ${pcstring('EXPORT.VERSION')} — ${pcstring('EXPORT.DATE')} — + Player: ${pcstring('PLAYERNAME')} — Character: ${pcstring('NAME')} +
diff --git a/components/header.ftl b/components/header.ftl new file mode 100644 index 0000000..db50c17 --- /dev/null +++ b/components/header.ftl @@ -0,0 +1,30 @@ + +
+

${pcstring('NAME')}

+ + + + + + + + + + + + + + + + + + + + + + + + + +
${pcstring('PLAYERNAME')}Lvl ${pcstring('TOTALLEVELS')} (${pcstring('EXP.CURRENT')} / ${pcstring('EXP.NEXT')})${pcstring('RACE')}${pcstring('GENDER')} (${pcstring('AGE')})${pcstring('HEIGHT')} (${pcstring('SIZELONG')})${pcstring('WEIGHT')}
PlayerLevel (XP)RaceGender (Age)Height (Size)Weight
${pcstring('DEITY')}, ${pcstring('ALIGNMENT')}${pcstring('LANGUAGES')}
Deity & AlignmentLanguages
+
diff --git a/components/inventory.ftl b/components/inventory.ftl new file mode 100644 index 0000000..c4a5bcf --- /dev/null +++ b/components/inventory.ftl @@ -0,0 +1,73 @@ +
+

Equipment

+
+ + + + + + + + + +<@loop from=0 to=pcvar('COUNT[EQUIPMENT.Not.Coin.NOT.Gem]-1') ; eq , eq_has_next> +<#assign eqType = pcstring("EQ.Not.Coin.NOT.Gem.${eq}.TYPE")?lower_case /> +<#assign eqQty = pcvar("EQ.Not.Coin.NOT.Gem.${eq}.QTY") /> +<#assign eqCharges = pcvar("EQ.Not.Coin.NOT.Gem.${eq}.CHARGES") /> +<#assign isUsable = eqType?contains("consumable") || eqType?contains("potion") || eqType?contains("ammunition") || eqType?contains("wand") || eqType?contains("scroll") || eqCharges gt 0 /> +<#assign eqSprop = pcstring('EQ.Not.Coin.NOT.Gem.${eq}.SPROP') /> +<#assign eqDesc = pcstring('EQ.Not.Coin.NOT.Gem.${eq}.DESC') /> +<#assign hasDetail = (eqSprop != "" || eqDesc != "") /> + + + + + + + + + <#if hasDetail> + + + + + + + + + + + +
ItemLocationQtyWeightCostUses (check off when used)
${pcstring('EQ.Not.Coin.NOT.Gem.${eq}.NAME.MAGIC~~')}${pcstring('EQ.Not.Coin.NOT.Gem.${eq}.LOCATION')}${pcstring('EQ.Not.Coin.NOT.Gem.${eq}.QTY')}${pcstring('EQ.Not.Coin.NOT.Gem.${eq}.WT')}${pcstring('EQ.Not.Coin.NOT.Gem.${eq}.COST')} + <#if isUsable> + <#if eqCharges gt 0> + <@loop from=1 to=eqCharges>☐ + <#else> + <@loop from=1 to=eqQty?int>☐ + + +
+ <#if (eqSprop != "")>Special: ${eqSprop}<#if (eqDesc != "")> — <#if (eqDesc != "")>${eqDesc} +
Totals:${pcstring('TOTAL.WEIGHT')}${pcstring('TOTAL.VALUE')}
+
+ Light load: no penalty — + Medium: −3 check penalty, max Dex +3, −10ft speed — + Heavy: −6 check penalty, max Dex +1, −10ft speed — + Lift overhead = heavy max; lift off ground = 2×; drag = 5× +
+
+
+ +
+
+
+ Encumbrance + ${pcstring('TOTAL.WEIGHT')} + Light: ${pcstring('WEIGHT.LIGHT')} / Med: ${pcstring('WEIGHT.MEDIUM')} / Heavy: ${pcstring('WEIGHT.HEAVY')} +
+
+ Unspent Gold + ${pcstring('GOLD.TRUNC')} gp +
+
+
diff --git a/components/notes.ftl b/components/notes.ftl new file mode 100644 index 0000000..3122200 --- /dev/null +++ b/components/notes.ftl @@ -0,0 +1,13 @@ + +
+

Notes (Description)

+
+ <#assign descRaw = pcstring('DESC') /> + <#assign descRaw = descRaw?replace("<","<") /> + <#assign descRaw = descRaw?replace(">",">") /> + <#assign descRaw = descRaw?replace("&","&") /> + <#assign descRaw = descRaw?replace(""",'"') /> + <#assign descRaw = descRaw?replace("'","'") /> + ${descRaw} +
+
diff --git a/components/portrait.ftl b/components/portrait.ftl new file mode 100644 index 0000000..bef0f08 --- /dev/null +++ b/components/portrait.ftl @@ -0,0 +1,8 @@ +<#if (pcstring('PORTRAIT') != '')> +
+

Portrait

+
+ ${pcstring('NAME')} +
+
+ diff --git a/components/prepared_spells.ftl b/components/prepared_spells.ftl new file mode 100644 index 0000000..c8866b9 --- /dev/null +++ b/components/prepared_spells.ftl @@ -0,0 +1,62 @@ +

Prepared Spells

+
+ Spell descriptions shown here come from PCGen's built-in effect summary. For the full text of any spell, refer to the source book listed in brackets — or search the spell name on the Archives of Nethys (aonprd.com) for free online access to the complete Pathfinder rules text. +
+<@loop from=2 to=pcvar('COUNT[SPELLBOOKS]-1') ; spellbook , spellbook_has_next> +<@loop from=pcvar('COUNT[SPELLRACE]') to=pcvar('COUNT[SPELLRACE]+COUNT[CLASSES]-1') ; class , class_has_next> +<#if (pcstring("SPELLLISTCLASS.${class}") != '')> +
+ ${pcstring('SPELLBOOKNAME.${spellbook}')} — ${pcstring('SPELLLISTCLASS.${class}')} +
+ <@loop from=0 to=9 ; level , level_has_next> + <#assign spelllevelcount = pcvar('COUNT[SPELLSINBOOK.${class}.${spellbook}.${level}]') /> + <#if (spelllevelcount > 0)> +
+
+ <#if (level == 0)>Cantrips (Level 0) — Unlimited Uses<#else>Level ${level} — Prepared: ${pcstring('SPELLLISTCAST.${class}.${level}')} +
+ + + + + + + + +<@loop from=0 to=spelllevelcount-1 ; spell , spell_has_next> + <#assign spSave = pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.SAVEINFO') /> + <#assign spDC = pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.DC') /> + <#assign spSR = pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.SR') /> + <#assign spSaveShort = spSave /> + <#if spSave?lower_case?contains("fortitude")><#assign spSaveShort = "Fortitude" /> + <#if spSave?lower_case?contains("reflex")><#assign spSaveShort = "Reflex" /> + <#if spSave?lower_case?contains("will")><#assign spSaveShort = "Will" /> + <#assign hasNoSave = (spSave = "None" || spSave = "" || spSave?lower_case = "none") /> + + + + + + + + +
SpellUsesSave, DC & SRRange, Time & DurationEffect / Description
+ ${pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.BONUSSPELL')}${pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.NAME')}
+ ${pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.SCHOOL')}
+ [${pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.SOURCE')}] +
+ <#if (level == 0)>∞<#else><@loop from=1 to=pcvar("SPELLMEM.${class}.${spellbook}.${level}.${spell}.TIMES")>☐ + + <#if !hasNoSave>${spSaveShort}
DC ${spDC}
+ SR: <#if (spSR != "")>${spSR}<#else>— +
+ ${pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.RANGE')}
+ ${pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.CASTINGTIME')}
+ ${pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.DURATION')} +
${pcstring('SPELLMEM.${class}.${spellbook}.${level}.${spell}.EFFECT')}
+
+ + + + + diff --git a/components/quick_view.ftl b/components/quick_view.ftl new file mode 100644 index 0000000..4050b6d --- /dev/null +++ b/components/quick_view.ftl @@ -0,0 +1,74 @@ +
+

Quick Reference Sheet

+ +
+
STR${pcstring('STAT.0.NOTEMP.NOEQUIP')}${pcstring('STAT.0.MOD.NOTEMP.NOEQUIP')}
+
DEX${pcstring('STAT.1.NOTEMP.NOEQUIP')}${pcstring('STAT.1.MOD.NOTEMP.NOEQUIP')}
+
CON${pcstring('STAT.2.NOTEMP.NOEQUIP')}${pcstring('STAT.2.MOD.NOTEMP.NOEQUIP')}
+
INT${pcstring('STAT.3.NOTEMP.NOEQUIP')}${pcstring('STAT.3.MOD.NOTEMP.NOEQUIP')}
+
WIS${pcstring('STAT.4.NOTEMP.NOEQUIP')}${pcstring('STAT.4.MOD.NOTEMP.NOEQUIP')}
+
CHA${pcstring('STAT.5.NOTEMP.NOEQUIP')}${pcstring('STAT.5.MOD.NOTEMP.NOEQUIP')}
+
+ + + + + + + + + + + + + +
HP${pcstring('HP')}
Init${pcstring('INITIATIVEMOD')}
Move<@loop from=0 to=pcvar('COUNT[MOVE]-1') ; mv , mv_has_next>${pcstring('MOVE.${mv}.RATE')}
AC${pcstring('AC.Total')}
Touch${pcstring('AC.Touch')}
Flat${pcstring('AC.Flatfooted')}
BAB${pcstring('ATTACK.MELEE')}
Melee${pcstring('ATTACK.MELEE.TOTAL')}
Ranged${pcstring('ATTACK.RANGED.TOTAL')}
+ + + + + + + + + + +
SaveTotalQuick Notes
Fortitude${pcstring('CHECK.0.TOTAL')}Poison, disease, body effects
Reflex${pcstring('CHECK.1.TOTAL')}Area effects, traps, avoid damage
Will${pcstring('CHECK.2.TOTAL')}Mental control, fear, compulsion
+ +
+
+ + + + + +<@loop from=0 to=(pcvar('count("SKILLSIT", "VIEW=VISIBLE_EXPORT")')/2)?int-1 ; skq1 , skq1_has_next> + class="shaded"> + + + + +
SkillTotal
${pcstring('SKILLSIT.${skq1}')}${pcstring('SKILLSIT.${skq1}.TOTAL')}
+
+
+ + + + + +<#assign skillCount = pcvar('count("SKILLSIT", "VIEW=VISIBLE_EXPORT")') /> +<#assign splitPoint = (skillCount/2)?int /> +<@loop from=splitPoint to=skillCount-1 ; skq2 , skq2_has_next> + class="shaded"> + + + + +
SkillTotal
${pcstring('SKILLSIT.${skq2}')}${pcstring('SKILLSIT.${skq2}.TOTAL')}
+
+
+ +
+ Quick use: keep this page visible during combat for AC, attacks, saves, and top skill totals. +
+
diff --git a/components/rules_reference.ftl b/components/rules_reference.ftl new file mode 100644 index 0000000..8e5154c --- /dev/null +++ b/components/rules_reference.ftl @@ -0,0 +1,35 @@ + +
+

Rules Reference

+
+ Rest
+ 8 hrs: Recover 1 HP / level + 1 point of ability damage.
+ 24 hrs: Same, but 2 HP and points.
+ With healer present/health check: Same, but 3 HP and points. +
+
+ House Rules
+ Level Up: Player and DM roll 1 Hit Die + Con modifier for extra HP. Highest value counts. +
+
+ Common Quick Rules
+ • Flanking: +2 attack.
+ • Aid Another: DC 10 check for ally +2 attack, AC, or check.
+ • Cover / Soft Cover: +4 AC (+2 Reflex); creatures can grant soft cover.
+ • Ranged Into Melee: −4 to hit. Target 2 size > ally? −2 to hit. 3 sizes or Precise Shot? No penalty.
+ • Concealment: Roll to hit and then 1d100 that should be greater than % of concealment (20%/50%).
+ • Casting Defensively: Concentration DC = 15 + (2 × spell level).
+ • DR / Resistance: DR reduces weapon damage; resistance reduces matching energy damage.
+ • Reach / Threatened Squares: you threaten where you can melee; leaving can provoke.
+ • Swift / Immediate: one per round; immediate uses next turn's swift.
+ • Criticals: Nat 20 threatens; confirm with another hit roll (not another 20).
+ • Dying / Stabilize: <0 HP lose 1 HP/round; stabilize check DC 10 + negative HP. +
+
+ Coins
+ Gold (gp): Most common coin piece
+ Silver (sp): 10sp = 1gp
+ Copper (cp): 100cp = 10sp = 1gp
+ Platinum (pp): 1pp = 10gp +
+
diff --git a/components/saves.ftl b/components/saves.ftl new file mode 100644 index 0000000..4093330 --- /dev/null +++ b/components/saves.ftl @@ -0,0 +1,44 @@ + +
+

Saves

+ + + + + + + + + +<@loop from=0 to=pcvar('COUNT[CHECKS]-1') ; chk , chk_has_next> + + + + + + + + + +
TotalBaseMagicMisc
+ ${pcstring('CHECK.${chk}.NAME')} + <#if (pcstring('CHECK.${chk}.NAME') = 'Fortitude')>
CON (${pcstring('CHECK.${chk}.STATMOD')})
+ <#if (pcstring('CHECK.${chk}.NAME') = 'Reflex')>
DEX (${pcstring('CHECK.${chk}.STATMOD')})
+ <#if (pcstring('CHECK.${chk}.NAME') = 'Will')>
WIS (${pcstring('CHECK.${chk}.STATMOD')})
+
${pcstring('CHECK.${chk}.TOTAL')}${pcstring('CHECK.${chk}.BASE')}${pcstring('CHECK.${chk}.MAGIC')}${pcstring('CHECK.${chk}.MISC.NOMAGIC.NOSTAT')}
+ <#assign hasSaveCond = pcvar('countdistinct("ABILITIES","ASPECT=SaveBonus")-1') /> + <#if (hasSaveCond >= 0)> +
+ Conditional Save Modifiers:
+ <@loop from=0 to=hasSaveCond ; ab , ab_has_next> + • ${pcstring('ABILITYALL.ANY.${ab}.ASPECT=SaveBonus.ASPECT.SaveBonus')}
+ +
+ +
+ Fortitude (CON): resists poison, disease, death effects — + Reflex (DEX): resists area effects, traps — + Will (WIS): resists mind-affecting, compulsion, fear — + DC set by the source ability or spell +
+
diff --git a/components/skills.ftl b/components/skills.ftl new file mode 100644 index 0000000..7d0a11e --- /dev/null +++ b/components/skills.ftl @@ -0,0 +1,70 @@ + +
+

Skills

+<#assign skillCount = pcvar('count("SKILLSIT", "VIEW=VISIBLE_EXPORT")') /> +<#assign skillHalf = (skillCount / 2)?int /> +
+
+ + + + + + + +<@loop from=0 to=skillHalf-1 ; sk , sk_has_next> + <#assign skMisc = pcvar('SKILLSIT.${sk}.MISC') /> + <#assign isClassSkill = (skMisc >= 3) /> + class="shaded"> + + + + + + +
SkillTotal+ Rnks+ Misc
+ <#if pcboolean("SKILLSIT.${sk}.UNTRAINED")>◆${pcstring('SKILLSIT.${sk}')}<#if (pcstring('SKILLSIT.${sk}.ACPv') != "v")>* +
${pcstring('SKILLSIT.${sk}.ABILITY')} (+${pcstring('SKILLSIT.${sk}.ABMOD')}) +
${pcstring('SKILLSIT.${sk}.TOTAL')}${pcstring("SKILLSIT.${sk}.RANK")?replace("\\.0","","rf")}${pcstring('SKILLSIT.${sk}.MISC')}
+
+
+ + + + + + + +<@loop from=skillHalf to=skillCount-1 ; sk , sk_has_next> + <#assign skMisc = pcvar('SKILLSIT.${sk}.MISC') /> + <#assign isClassSkill = (skMisc >= 3) /> + class="shaded"> + + + + + + +
SkillTotal+ Rnks+ Misc
+ <#if pcboolean("SKILLSIT.${sk}.UNTRAINED")>◆${pcstring('SKILLSIT.${sk}')}<#if (pcstring('SKILLSIT.${sk}.ACPv') != "v")>* +
${pcstring('SKILLSIT.${sk}.ABILITY')} (+${pcstring('SKILLSIT.${sk}.ABMOD')}) +
${pcstring('SKILLSIT.${sk}.TOTAL')}${pcstring("SKILLSIT.${sk}.RANK")?replace("\\.0","","rf")}${pcstring('SKILLSIT.${sk}.MISC')}
+
+
+
Bold = class skill with rank (+3 bonus)   ◆ = usable untrained   * = armor check penalty applies
+
+ Class skills give +3 if you have ≥1 rank — + Max ranks = character level — + Take 10: non-stressful, treat roll as 10 — + Take 20: 20× longer, treat roll as 20 (no fail consequence) +
+
+ Conditional Skill Modifiers:
+ <#assign hasSkillCond = false /> + <@loop from=0 to=pcvar('countdistinct("ABILITIES","ASPECT=SkillBonus")-1') ; ab , ab_has_next> + <#assign hasSkillCond = true /> + • ${pcstring('ABILITYALL.ANY.${ab}.ASPECT=SkillBonus.ASPECT.SkillBonus')}
+ + <#if !hasSkillCond>No modifiers +
+
diff --git a/components/special_attacks.ftl b/components/special_attacks.ftl new file mode 100644 index 0000000..ceae23a --- /dev/null +++ b/components/special_attacks.ftl @@ -0,0 +1,28 @@ +
+

Special Attacks

+
+ + <#if (pcvar('countdistinct("ABILITIES","CATEGORY=Special Ability","TYPE=SpecialAttack","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")') > 0)> + <@loop from=0 to=pcvar('countdistinct("ABILITIES","CATEGORY=Special Ability","TYPE=SpecialAttack","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")-1') ; sa , sa_has_next> + + + + + + +
+ ${pcstring('ABILITYALL.Special Ability.VISIBLE.${sa}.TYPE=SpecialAttack')}
+ [${pcstring('ABILITYALL.Special Ability.VISIBLE.${sa}.TYPE=SpecialAttack.SOURCE')}]
+ ${pcstring('ABILITYALL.Special Ability.VISIBLE.${sa}.TYPE=SpecialAttack.DESC')} +
+ <#assign saUses = pcstring('ABILITYALL.Special Ability.VISIBLE.${sa}.TYPE=SpecialAttack.ASPECT.UsesPerDay') /> + <#if (saUses != "")> + Uses/day: ${saUses}
+ <#assign saUsesN = pcvar('ABILITYALL.Special Ability.VISIBLE.${sa}.TYPE=SpecialAttack.ASPECT.UsesPerDay.INTVAL') /> + <#if (saUsesN > 0)> + <@loop from=1 to=saUsesN>☐ + + +
+
+
diff --git a/components/special_qualities.ftl b/components/special_qualities.ftl new file mode 100644 index 0000000..b315a06 --- /dev/null +++ b/components/special_qualities.ftl @@ -0,0 +1,83 @@ +
+ + +
+<#if (pcvar('countdistinct("ABILITIES","CATEGORY=Special Ability","TYPE=SpecialQuality","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")') > 0)> +
+

Special Qualities

+
+ +<@loop from=0 to=pcvar('countdistinct("ABILITIES","CATEGORY=Special Ability","TYPE=SpecialQuality","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")-1') ; sq , sq_has_next> + + + + +
+ ${pcstring('ABILITYALL.Special Ability.VISIBLE.${sq}.TYPE=SpecialQuality')}
+ [${pcstring('ABILITYALL.Special Ability.VISIBLE.${sq}.TYPE=SpecialQuality.SOURCE')}]
+ ${pcstring('ABILITYALL.Special Ability.VISIBLE.${sq}.TYPE=SpecialQuality.DESC')} +
+
+
+ +
+ + +
+
+

Feats

+
+ +<@loop from=0 to=pcvar('countdistinct("ABILITIES","CATEGORY=FEAT","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")-1') ; ft , ft_has_next> + + + + +
+ ${pcstring('ABILITYALL.Feat.VISIBLE.${ft}')}
+ [${pcstring('ABILITYALL.Feat.VISIBLE.${ft}.SOURCE')}]
+ ${pcstring('ABILITYALL.Feat.VISIBLE.${ft}.BENEFIT')} +
+
+
+ +<#if (pcvar('count("ABILITIES","CATEGORY=Special Ability","TYPE=Trait","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")') > 0)> +
+

Traits

+
+ +<@loop from=0 to=pcvar('count("ABILITIES","CATEGORY=Special Ability","TYPE=Trait","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")-1') ; tr , tr_has_next> + + + + +
+ ${pcstring('ABILITYALL.Special Ability.VISIBLE.${tr}.TYPE=Trait')}
+ [${pcstring('ABILITYALL.Special Ability.VISIBLE.${tr}.TYPE=Trait.SOURCE')}]
+ ${pcstring('ABILITYALL.Special Ability.VISIBLE.${tr}.TYPE=Trait.DESC')} +
+
+
+ + +<#if (pcstring('DOMAIN.1') != '')> +
+

Domains

+
+ +<@loop from=1 to=pcvar('COUNT[DOMAINS]') ; dm , dm_has_next> + + + + +
+ ${pcstring('DOMAIN.${dm}')}
+ ${pcstring('DOMAIN.${dm}.POWER')} +
+
+
+ + +
+ +
diff --git a/components/spellbook.ftl b/components/spellbook.ftl new file mode 100644 index 0000000..fff0b83 --- /dev/null +++ b/components/spellbook.ftl @@ -0,0 +1,64 @@ + +<@loop from=pcvar('COUNT[SPELLRACE]') to=pcvar('COUNT[SPELLRACE]+COUNT[CLASSES]-1') ; class , class_has_next> +<#if (pcstring("SPELLLISTCLASS.${class}") != '')> + <#assign knownCount = 0 /> + <@loop from=0 to=9 ; level , level_has_next> + <#assign knownCount = knownCount + pcvar('COUNT[SPELLSINBOOK.${class}.0.${level}]') /> + + <#if (knownCount > 0)> +

Known Spells — ${pcstring('SPELLLISTCLASS.${class}')}

+
+ Known spells are available at any time and cast spontaneously. Spells per day (uses) are shown per level. +
+ <@loop from=0 to=9 ; level , level_has_next> + <#assign spelllevelcount = pcvar('COUNT[SPELLSINBOOK.${class}.0.${level}]') /> + <#if (spelllevelcount > 0)> +
+
+ <#if (level == 0)>Cantrips (Level 0) — Unlimited Uses<#else>Level ${level} — Spells/Day: ${pcstring('SPELLLISTCAST.${class}.${level}')} +
+ + + + + + + + +<@loop from=0 to=spelllevelcount-1 ; spell , spell_has_next> + <#assign spSave = pcstring('SPELLMEM.${class}.0.${level}.${spell}.SAVEINFO') /> + <#assign spDC = pcstring('SPELLMEM.${class}.0.${level}.${spell}.DC') /> + <#assign spSR = pcstring('SPELLMEM.${class}.0.${level}.${spell}.SR') /> + <#assign spSaveShort = spSave /> + <#if spSave?lower_case?contains("fortitude")><#assign spSaveShort = "Fortitude" /> + <#if spSave?lower_case?contains("reflex")><#assign spSaveShort = "Reflex" /> + <#if spSave?lower_case?contains("will")><#assign spSaveShort = "Will" /> + <#assign hasNoSave = (spSave = "None" || spSave = "" || spSave?lower_case = "none") /> + + + + + + + + +
SpellUsesSave, DC & SRRange, Time & DurationEffect / Description
+ ${pcstring('SPELLMEM.${class}.0.${level}.${spell}.NAME')}
+ ${pcstring('SPELLMEM.${class}.0.${level}.${spell}.SCHOOL')}
+ [${pcstring('SPELLMEM.${class}.0.${level}.${spell}.SOURCE')}] +
+ <#if (level == 0)>∞<#else>${pcstring('SPELLLISTCAST.${class}.${level}')} + + <#if !hasNoSave>${spSaveShort}
DC ${spDC}
+ SR: <#if (spSR != "")>${spSR}<#else>— +
+ ${pcstring('SPELLMEM.${class}.0.${level}.${spell}.RANGE')}
+ ${pcstring('SPELLMEM.${class}.0.${level}.${spell}.CASTINGTIME')}
+ ${pcstring('SPELLMEM.${class}.0.${level}.${spell}.DURATION')} +
${pcstring('SPELLMEM.${class}.0.${level}.${spell}.EFFECT')}
+
+ + + + + diff --git a/components/weapons.ftl b/components/weapons.ftl new file mode 100644 index 0000000..7cd4aa9 --- /dev/null +++ b/components/weapons.ftl @@ -0,0 +1,112 @@ +
+

Weapons

+
+ + + + + + + + + +<@loop from=0 to=pcvar('COUNT[EQTYPE.Weapon]-1') ; wp , wp_has_next> +<#if (pcstring("WEAPON.${wp}.NAME") != "")> +<#assign wCat = pcstring("WEAPON.${wp}.CATEGORY")?lower_case /> +<#assign isRanged = wCat?contains("ranged") /> +<#assign isLight = pcboolean("WEAPON.${wp}.ISTYPE.Light") /> +<#assign wType = pcstring("WEAPON.${wp}.TYPE")?lower_case /> +<#assign wTypeFull = "" /> +<#if wType?contains("p")><#assign wTypeFull = wTypeFull + "Piercing " /> +<#if wType?contains("s")><#assign wTypeFull = wTypeFull + "Slashing " /> +<#if wType?contains("b")><#assign wTypeFull = wTypeFull + "Bludgeoning" /> +<#assign wTypeFull = wTypeFull?trim /> + + + <#if isRanged> + + + <#else> + + + + + + + + <#if isRanged> + + + + + <#if (pcstring('WEAPON.${wp}.SPROP') != "")> + + + + + + +<#assign charSize = pcstring('SIZE')?lower_case /> +<#assign sizeKey = charSize /> +<#if charSize?starts_with("tiny")><#assign sizeKey = "t" /> +<#elseif charSize?starts_with("small")><#assign sizeKey = "s" /> +<#elseif charSize?starts_with("medium")><#assign sizeKey = "m" /> +<#elseif charSize?starts_with("large")><#assign sizeKey = "l" /> +<#elseif charSize?starts_with("huge")><#assign sizeKey = "h" /> +<#elseif charSize?starts_with("gargantuan")><#assign sizeKey = "g" /> +<#elseif charSize?starts_with("colossal")><#assign sizeKey = "c" /> + +<#assign unarmedDie = "1d3" /> +<#if sizeKey == "t" || sizeKey == "s"><#assign unarmedDie = "1d2" /> +<#elseif sizeKey == "m"><#assign unarmedDie = "1d3" /> +<#elseif sizeKey == "l"><#assign unarmedDie = "1d4" /> +<#elseif sizeKey == "h"><#assign unarmedDie = "1d6" /> +<#elseif sizeKey == "g"><#assign unarmedDie = "1d8" /> +<#elseif sizeKey == "c"><#assign unarmedDie = "2d6" /> + +<#assign strMod = pcstring('STAT.0.MOD.SIGN') /> + + + + + + + + +
Weapon1H-P2H2W-PDMG (2H)Crit
+ ${pcstring('WEAPON.${wp}.NAME')}<#if isLight> △
+ ${wTypeFull} +
${pcstring('WEAPON.${wp}.TOTALHIT')} (ranged, ${pcstring('WEAPON.${wp}.RANGE')})${pcstring('WEAPON.${wp}.DAMAGE')}${pcstring('WEAPON.${wp}.BASEHIT')}${pcstring('WEAPON.${wp}.THHIT')}${pcstring('WEAPON.${wp}.TWPHITH')} + ${pcstring('WEAPON.${wp}.BASICDAMAGE')}
+ 2H: ${pcstring('WEAPON.${wp}.THDAMAGE')} +
${pcstring('WEAPON.${wp}.CRIT')}/x${pcstring('WEAPON.${wp}.MULT')}
+ Range increments (−2 per increment): +  |  2x: ${pcstring('WEAPON.${wp}.RANGELIST.1.TOTALHIT')} +  |  3x: ${pcstring('WEAPON.${wp}.RANGELIST.2.TOTALHIT')} +  |  4x: ${pcstring('WEAPON.${wp}.RANGELIST.3.TOTALHIT')} +  |  5x: ${pcstring('WEAPON.${wp}.RANGELIST.4.TOTALHIT')} +  (max 5) +
+ Special: ${pcstring('WEAPON.${wp}.SPROP')} +
+ Unarmed
+ Bludgeoning +
${pcstring('ATTACK.MELEE.TOTAL')}${pcstring('ATTACK.MELEE.TOTAL')}${pcstring('ATTACK.MELEE.TOTAL')} + ${unarmedDie}${strMod}
+ 2H: ${unarmedDie}${strMod} +
20/x2
+
+ 1H-P = one-handed primary   + 2H = two-handed, adds 1.5× Str to damage   + 2W-P = two weapons, primary hand   + △ = light weapon (Small or Tiny size) +
+
+ Two-weapon attack penalties — + 2W-P: −4 (or −2 with TWF feat)   + 2W-O(L) light off-hand: −4/−4 (or −2/−2 with TWF feat)   + 2W-O(H) heavy off-hand: −4/−8 (or −2/−6 with TWF feat) — + Power Attack: −1 hit per 4 BAB for +2 dmg (+3 two-handed) +
+
+
diff --git a/scripts/build_sheets.py b/scripts/build_sheets.py new file mode 100644 index 0000000..46a37b0 --- /dev/null +++ b/scripts/build_sheets.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +from pathlib import Path +import re + +ROOT = Path(__file__).resolve().parent.parent +TEMPLATES_DIR = ROOT / "templates" +COMPONENTS_DIR = ROOT / "components" +OUTPUT_DIR = ROOT / "OutputSheets" +TOKEN_RE = re.compile(r"\{\{\s*component:([a-zA-Z0-9_-]+)\s*\}\}") +MAX_EXPANSION_DEPTH = 20 # Guard against cyclic/nested references; normal templates should resolve in a few passes + + +def load_components() -> dict[str, str]: + components: dict[str, str] = {} + for path in sorted(COMPONENTS_DIR.glob("*.ftl")): + components[path.stem] = path.read_text(encoding="utf-8") + return components + + +def expand_components(content: str, components: dict[str, str]) -> str: + expanded = content + for _ in range(MAX_EXPANSION_DEPTH): + changed = False + + def repl(match: re.Match[str]) -> str: + nonlocal changed + name = match.group(1) + if name not in components: + raise ValueError(f"Missing component: {name}") + changed = True + return components[name] + + new_expanded = TOKEN_RE.sub(repl, expanded) + expanded = new_expanded + if not changed: + break + if TOKEN_RE.search(expanded): + raise ValueError(f"Unresolved component tokens remain after expansion: {TOKEN_RE.findall(expanded)}") + return expanded + + +def build() -> None: + components = load_components() + if not components: + raise ValueError("No components found in components/") + + for template_path in sorted(TEMPLATES_DIR.rglob("*.ftl")): + rel = template_path.relative_to(TEMPLATES_DIR) + output_path = OUTPUT_DIR / rel + output_path.parent.mkdir(parents=True, exist_ok=True) + template_content = template_path.read_text(encoding="utf-8") + output_content = expand_components(template_content, components) + output_path.write_text(output_content, encoding="utf-8") + print(f"Built {output_path.relative_to(ROOT)}") + + +if __name__ == "__main__": + build() diff --git a/templates/d20/fantasy/xmlhtml/csheet_known_spells.htm.ftl b/templates/d20/fantasy/xmlhtml/csheet_known_spells.htm.ftl new file mode 100644 index 0000000..beb6a0e --- /dev/null +++ b/templates/d20/fantasy/xmlhtml/csheet_known_spells.htm.ftl @@ -0,0 +1,158 @@ +<#ftl encoding="UTF-8" strip_whitespace=true> + + + + +${pcstring('NAME')} + + + +{{ component:header }} +{{ component:ability_scores }} +{{ component:combat }} +{{ component:saves }} +{{ component:notes }} +{{ component:skills }} +{{ component:special_qualities }} +{{ component:special_attacks }} +{{ component:weapons }} +{{ component:combat_maneuvers }} +{{ component:combat_conditionals }} +{{ component:armor_shields }} +{{ component:spellbook }} +{{ component:prepared_spells }} +{{ component:concentration_reference }} +{{ component:inventory }} +{{ component:rules_reference }} +{{ component:aoo_reference }} +{{ component:ability_influence }} +{{ component:common_conditions }} +{{ component:quick_view }} +{{ component:biography }} +{{ component:portrait }} +{{ component:footer }} + + + diff --git a/templates/d20/fantasy/xmlhtml/csheet_prepared_spells.htm.ftl b/templates/d20/fantasy/xmlhtml/csheet_prepared_spells.htm.ftl new file mode 100644 index 0000000..f9b8f9e --- /dev/null +++ b/templates/d20/fantasy/xmlhtml/csheet_prepared_spells.htm.ftl @@ -0,0 +1,157 @@ +<#ftl encoding="UTF-8" strip_whitespace=true> + + + + +${pcstring('NAME')} + + + +{{ component:header }} +{{ component:ability_scores }} +{{ component:combat }} +{{ component:saves }} +{{ component:notes }} +{{ component:skills }} +{{ component:special_qualities }} +{{ component:special_attacks }} +{{ component:weapons }} +{{ component:combat_maneuvers }} +{{ component:combat_conditionals }} +{{ component:armor_shields }} +{{ component:prepared_spells }} +{{ component:concentration_reference }} +{{ component:inventory }} +{{ component:rules_reference }} +{{ component:aoo_reference }} +{{ component:ability_influence }} +{{ component:common_conditions }} +{{ component:quick_view }} +{{ component:biography }} +{{ component:portrait }} +{{ component:footer }} + + +