From a426cbd5207d7e68bbbbff2267ca233dc77ed812 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sat, 18 Nov 2023 03:45:15 -0500 Subject: [PATCH] feat: create lcars countdown panel --- src/module/actor/army/data.ts | 1 - src/module/actor/army/sheet.ts | 37 ++ src/module/actor/army/values.ts | 20 +- src/scripts/handlebars.ts | 4 +- src/scripts/register-sheets.ts | 8 + src/styles/actor/_index.scss | 2 + src/styles/actor/army/_index.scss | 481 +++++++++++++++++++++ static/lang/kingmaker-en.json | 116 ++--- static/templates/actors/army/gear-card.hbs | 30 ++ static/templates/actors/army/sheet.hbs | 334 ++++++++++++++ 10 files changed, 952 insertions(+), 81 deletions(-) create mode 100644 src/module/actor/army/sheet.ts create mode 100644 src/styles/actor/army/_index.scss create mode 100644 static/templates/actors/army/gear-card.hbs create mode 100644 static/templates/actors/army/sheet.hbs diff --git a/src/module/actor/army/data.ts b/src/module/actor/army/data.ts index 6a8a1b6bfdc..617f471b60c 100644 --- a/src/module/actor/army/data.ts +++ b/src/module/actor/army/data.ts @@ -78,7 +78,6 @@ interface ArmyDetailsSource extends Required { strongSave: string; weakSave: string; description: string; - blurb: string; } interface ArmySystemData extends Omit, ActorSystemData { diff --git a/src/module/actor/army/sheet.ts b/src/module/actor/army/sheet.ts new file mode 100644 index 00000000000..bdf5ee95896 --- /dev/null +++ b/src/module/actor/army/sheet.ts @@ -0,0 +1,37 @@ +import { ActorSheetPF2e } from "@actor/sheet/base.ts"; +import { ArmyPF2e } from "./document.ts"; +import { ActorSheetDataPF2e } from "@actor/sheet/data-types.ts"; +import { Alignment } from "./types.ts"; +import { ALIGNMENTS, ARMY_TYPES } from "./values.ts"; +import { kingmakerTraits } from "@scripts/config/traits.ts"; +import * as R from "remeda"; + +class ArmySheetPF2e extends ActorSheetPF2e { + static override get defaultOptions(): ActorSheetOptions { + const options = super.defaultOptions; + return { + ...options, + classes: [...options.classes, "army"], + template: "systems/pf2e/templates/actors/army/sheet.hbs", + }; + } + + override async getData(options?: Partial): Promise { + const data = await super.getData(options); + + return { + ...data, + alignments: ALIGNMENTS, + armyTypes: R.pick(kingmakerTraits, ARMY_TYPES), + rarityTraits: CONFIG.PF2E.rarityTraits, + }; + } +} + +interface ArmySheetData extends ActorSheetDataPF2e { + alignments: Iterable; + armyTypes: Record; + rarityTraits: Record; +} + +export { ArmySheetPF2e }; diff --git a/src/module/actor/army/values.ts b/src/module/actor/army/values.ts index 29c290e1f4a..039ca316710 100644 --- a/src/module/actor/army/values.ts +++ b/src/module/actor/army/values.ts @@ -24,28 +24,36 @@ function fetchArmyGearData(gearType: String): Object { ? "icons/weapons/axes/axe-battle-black.webp" : "icons/weapons/crossbows/crossbow-simple-brown.webp", name: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Weapons.name"), - level: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Weapons.level"), description: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Weapons.description"), traits: ["army", "magical"], - ranks: 3, + level: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Weapons.level"), + ranks: [ + { price: 20, level: 2 }, + { price: 40, level: 10 }, + { price: 60, level: 16 }, + ], }; case "potions": return { img: "icons/consumables/potions/bottle-round-corked-orante-red.webp", name: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Potions.name"), - level: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Potions.level"), description: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Potions.description"), - price: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Potions.price"), traits: ["army", "consumable", "healing", "magical", "potion"], + level: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Potions.level"), + price: 15, }; case "armor": return { img: "icons/equipment/shield/heater-wooden-brown-axe.webp", name: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Armor.name"), - level: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Armor.level"), description: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Armor.description"), traits: ["army", "magical"], - ranks: 3, + level: game.i18n.localize("PF2E.Kingmaker.Army.Gear.Armor.level"), + ranks: [ + { price: 25, level: 5 }, + { price: 50, level: 11 }, + { price: 75, level: 18 }, + ], }; default: return {}; diff --git a/src/scripts/handlebars.ts b/src/scripts/handlebars.ts index de3442bf0f8..1ff32738398 100644 --- a/src/scripts/handlebars.ts +++ b/src/scripts/handlebars.ts @@ -69,7 +69,9 @@ export function registerHandlebarsHelpers(): void { }); Handlebars.registerHelper("times", (count: unknown, options: Handlebars.HelperOptions): string => - [...Array(Number(count)).keys()].map((i) => options.fn(i, { data: options.data, blockParams: [i] })).join(""), + [...Array(Number(count) || 0).keys()] + .map((i) => options.fn(i, { data: options.data, blockParams: [i] })) + .join(""), ); Handlebars.registerHelper("concat", (...params: unknown[]): string => { diff --git a/src/scripts/register-sheets.ts b/src/scripts/register-sheets.ts index b48450cc3e3..416dca2e7d0 100644 --- a/src/scripts/register-sheets.ts +++ b/src/scripts/register-sheets.ts @@ -33,6 +33,7 @@ import { UserConfigPF2e } from "@module/user/sheet.ts"; import { SceneConfigPF2e } from "@scene/sheet.ts"; import { TokenDocumentPF2e } from "@scene/token-document/document.ts"; import { TokenConfigPF2e } from "@scene/token-document/sheet.ts"; +import { ArmySheetPF2e } from "@actor/army/sheet.ts"; export function registerSheets(): void { const sheetLabel = game.i18n.localize("PF2E.SheetLabel"); @@ -101,6 +102,13 @@ export function registerSheets(): void { makeDefault: true, }); + // Army + Actors.registerSheet("pf2e", ArmySheetPF2e, { + types: ["army"], + label: game.i18n.format(sheetLabel, { type: localizeType("army") }), + makeDefault: true, + }); + // ITEM Items.unregisterSheet("core", ItemSheet); diff --git a/src/styles/actor/_index.scss b/src/styles/actor/_index.scss index 320586f4d2f..7648d527bb1 100644 --- a/src/styles/actor/_index.scss +++ b/src/styles/actor/_index.scss @@ -356,6 +356,8 @@ $header-height: 89px; /* Adjust height of the header */ @import "party"; +@import "army"; + /* Mystification data revealed to GMs */ .gm-mystified-data { opacity: 0.75; diff --git a/src/styles/actor/army/_index.scss b/src/styles/actor/army/_index.scss new file mode 100644 index 00000000000..d7c937df526 --- /dev/null +++ b/src/styles/actor/army/_index.scss @@ -0,0 +1,481 @@ +// todo: convert to css variables +$faded-color: #7a7971; +$color: rgb(68, 0, 0); // Was previously "rarity-common" but now that throws an error, diagnose? + +.sheet.army { + form { + display: grid; + + grid-template: + "header header" min-content + "sidebar content" 1fr + / min-content 1fr; + } + + form > header { + background: url("/assets/sheet/header-bw.webp"), url("/assets/sheet/background.webp"); + background-repeat: repeat-x, no-repeat; + background-size: cover; + background-color: #d9d3d2; + background-blend-mode: multiply; + + color: var(--text-light); + align-items: center; + display: flex; + grid-area: header; + gap: 0.5rem; + + min-height: 5.5rem; + padding: 0 0.75rem; + + .frame { + position: relative; + width: 4.25rem; + height: 4.25rem; + + img { + object-fit: cover; + object-position: top; + border: none; + border-radius: 0; + width: 4.25rem; + height: 4.25rem; + cursor: pointer; + @include brown-border; + } + } + + .details { + display: grid; + flex: 1; + align-items: center; + grid-template: + "name level" min-content + "traits level" min-content + / 1fr min-content; + + input[type="text"], + input[type="number"] { + color: var(--text-light); + border: none; + border-bottom: 1px solid transparent; + padding: 0; + height: unset; + &:hover, + &:focus { + border: none; + border-bottom: 1px solid var(--text-light); + box-shadow: none; + } + &::placeholder { + color: #bbb; + opacity: 0.4; + } + } + + .name { + grid-area: name; + + font-family: var(--sans-serif-condensed); + font-size: var(--font-size-30); + font-weight: 700; + width: 100%; + max-width: calc(100% - 5.5rem); + font-variant: small-caps; + text-indent: 0.5rem; + margin-right: 18px; + } + + .tags { + grid-area: traits; + padding: 0 0.5rem; + } + + .level-label { + grid-area: level; + + display: block; + min-width: 9rem; + text-align: right; + margin-right: 0.1em; + text-transform: uppercase; + font-family: var(--serif-condensed); + font-size: var(--font-size-28); + font-weight: 700; + input.level { + width: 2ch; + text-align: center; + margin: 0 0.25rem; + } + } + } + } + + // ============= // + // SIDEBAR // + // ============= // + aside.army-sidebar { + color: $color; + width: 10rem; + display: flex; + flex-direction: column; + gap: 1rem; + text-align: center; + padding: 0.5rem; + overflow: hidden scroll; + + // All sidebar sections + section.sidebar-section { + width: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + gap: 0.25rem; + font-size: 1rem; + //Rows within the sections + div { + line-height: 1rem; + display: flex; + width: 100%; + flex-direction: row; + justify-content: space-between; + align-items: baseline; + //The header row + &.section-head { + border-bottom: 1px solid; + border-color: $faded-color; + align-items: baseline; + line-height: 18px; + h4 { + font-weight: bold; + margin-bottom: 0px; + font-size: 0.9rem; + padding-left: 4px; + width: 100%; + text-align: left; + } + } + // Rows with buttons + &.buttons { + & > * { + flex: 1 1 25%; + } + label { + justify-content: center; + } + } + } + //Headers and labels + label { + justify-content: space-between; + font-size: 0.8rem; + height: 20px; + line-height: 20px; + width: 100%; + display: flex; + margin: auto; + } + a.roll-icon { + flex-direction: row; + align-items: baseline; + display: flex; + } + } + //The portrait section + section.image-container { + border: none; + position: relative; + img.profile-img { + width: 100%; + height: 100%; + margin: 0 auto; + border-radius: 10px; + border: unset; + } + a.hover-icon { + position: absolute; + bottom: 0; + right: 0; + } + } + } + + // Sheet contents + section.army-body { + color: $color; + padding: 0.5rem; + overflow: auto; + + fieldset { + border: 1px solid #5e0000; + border-radius: 3px; + margin: 8px 0; + legend { + font-size: 1rem; + color: rgb(95, 0, 0); + text-transform: bold; + } + } + + label { + display: flex; + align-items: center; + position: relative; + } + + // ============= // + // STRIKES // + // ============= // + section.weapons { + display: flex; + justify-content: space-between; + gap: 1rem; + fieldset.weapons, + .upgrades { + display: flex; + flex-direction: column; + justify-content: space-evenly; + .strike { + flex: 0; + display: flex; + gap: 0.25rem; + justify-content: space-between; + align-items: center; + line-height: 1.5rem; + a.melee, + a.ranged { + border: 1px solid rgba(200, 100, 60, 0.15); + border-radius: 4px; + background: rgba(255, 255, 255, 0.25); + text-align: center; + &[data-attack="0"] { + width: 3rem; + line-height: 1.25rem; + } + &[data-attack="1"], + &[data-attack="2"] { + font-size: 0.7rem; + width: 1.5rem; + line-height: 1rem; + } + } + label.roll { + display: flex; + } + input.name { + flex: 1; + text-indent: 0.25rem; + height: 1.5rem; + } + } + } + fieldset.weapons { + flex: 1; + } + } + // ============= // + // ITEMS // + // ============= // + ol.item-list { + list-style-type: none; + padding: 0; + li.item { + display: flex; + align-items: center; + line-height: 1.5rem; + &:nth-child(odd) { + background: rgba(180, 175, 175, 0.25); + } + &.expanded { + flex-wrap: wrap; + .tags { + line-height: 1rem; + padding: 0.5rem 0 0 0; + } + } + .item-icon { + border-radius: 6px; + border: none; + max-height: 1.5rem; + max-width: 1.5rem; + position: relative; + } + .item-name { + display: flex; + text-indent: 0.5rem; + font-weight: bold; + flex: 1 1; + h4 { + margin: auto 0; + } + } + .item-frequency { + flex: 0.2 0 4rem; + gap: 0.25rem; + display: flex; + input { + text-align: right; + } + input, + span { + flex: 1; + margin: auto; + } + } + .item-controls i { + flex: 0 0 4rem; + } + .item-description { + color: black; + line-height: 1.25rem; + } + } + } + } + + //Sidebar Buttons & inputs + .army-sidebar input { + flex: 0; + min-width: 2em; + font-size: 0.9rem; + &[type="number"] { + width: 32px; + } + &[type="text"] { + text-align: left; + font-size: 0.8rem; + } + &[type="checkbox"] { + font-size: var(--font-size-10); + height: 12px; + margin: 2px; + padding: 0; + } + } + + button:hover, + .item-chat:hover { + text-shadow: 0 0 5px var(--color-shadow-highlight); + box-shadow: none; + cursor: pointer; + } + a.item-chat .imgicon { + position: absolute; + background: beige; + width: 1.5rem; + font-size: 1rem; + opacity: 0.5; + z-index: 1; + line-height: 1.5rem; + } + + button { + height: 100%; + width: 2rem; + font-size: 14px; + margin: auto; + align-items: center; + border: none; + background: none; + label, + i { + margin: auto; + } + &.sheet-settings button { + font-size: 1rem; + width: 3rem; + margin: 0; + } + &.pips { + width: 3.5rem; + height: 24px; + display: flex; + flex-flow: row nowrap; + justify-content: center; + } + // Empty pips + .empty { + color: darkgrey; + } + // Filled pips + .filled { + color: black; + } + // Disabled buttons + .disabled { + color: darkgrey; + i::before { + text-shadow: none; + cursor: default; + } + } + } + + // Buttons that should be hidden unless the parent element is hovered + li.item .item-controls i, + a.item-icon i.imgicon { + visibility: hidden; + } + li.item:hover .item-controls i, + a.item-icon:hover i.imgicon { + visibility: unset; + } + + section.roll { + display: flex; + align-items: center; + min-width: 4rem; + justify-content: space-between; + i { + min-width: 1.5rem; + font-size: 1rem; + } + input.roll-bonus { + text-align: center; + width: 2rem; + height: 1.5rem; + } + } + + span.plus { + font-size: 0.75rem; + } + + legend.compendium-items:hover { + cursor: pointer; + text-shadow: 0 0 5px var(--color-shadow-highlight); + } + + .placeholder { + color: gray; + } + .routed { + color: darkred; + } + + .flex { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + label { + text-align: left; + align-items: center; + } + } + + i.fa-dice-d20 { + opacity: 0.75; + &:hover { + opacity: 1; + } + } + + // "manual" vs "automatic" statgen mode + input[type="number"], + input.name { + background: rgba(0, 0, 0, 0.05); + border-radius: 4px; + } +} diff --git a/static/lang/kingmaker-en.json b/static/lang/kingmaker-en.json index 13f8e597fcf..79f9ec7d8b3 100644 --- a/static/lang/kingmaker-en.json +++ b/static/lang/kingmaker-en.json @@ -8,61 +8,7 @@ }, "AbilityScores": "Ability Scores", "Army": { - "routThreshold": "Rout Threshold", - "scouting": "Scouting", - "morale": "Morale", - "maneuver": "Maneuver", - "maxTactics": "Max Tactics", - "consumption": "Consumption", - "standardDC": "Standard DC", - "recruitmentDC": "Recruitment DC", - "ammunition": "Ammunition", - "ammunitionMax": "Max Ammunition", - "newArmyName": "New Army", - "attributesHeader": "Attributes", - "strikesHeader": "Strikes", - "upgradesHeader": "Potency", - "routThresholdExceeded": "Army's HP is below Rout Threshold!", - "placeholderTacticsText": "Drag tactics here to add!", - "Strikes": { - "melee": "Melee Strike", - "ranged": "Ranged Strike", - "proficiency": "Proficiency", - "potency": "Potency", - "ammunitionLabel": "Ammunition: " - }, - "ToggleLock": { - "lockSheet": "Lock Input Fields", - "unlockSheet": "Unlock Input Fields", - "lockWeapon": "Weapon is available", - "unlockWeapon": "Weapon is unavailable" - }, - "InfoButton": { - "tooltipPotion" : "Potions Info", - "tooltipRanged" : "Ranged Weapons Info", - "tooltipMelee" : "Melee Weapons Info", - "tooltipArmour" : "Armour Info", - "levelLabel": "Level: ", - "priceLabel": "Price: ", - "resourceLabel": " RP" - }, - "Potions": { - "label": "Potion(s)", - "drinkTooltip": "Use Potion (+1 HP)", - "drinkErrorNoPotions": "No potions available", - "drinkErrorFullHP": "HP is already full" - }, - "StatGenerator": { - "title": "Generate Army Stats", - "desc": "Generates stats for armies using the default values as defined in the Warfare rules. New statistics will be dependent on the level. Can be used for creating new armies or for leveling up existing ones. Gear, tactics, actions, and traits will not be replaced.", - "warning": "Warning: The old statistics will be permanently overwritten.", - "parametersHeader": "Parameters", - "levelLabel": "Level: ", - "saveLabel": "Strong save: ", - "confirmButton": "Update Stats", - "levelUpTitle": "Level Up Army", - "levelUpDesc": "Increases Army level by 1, updating stats using the default values as defined in the Warfare rules. New statistics will be dependent on the level. Gear, tactics, actions, and traits will not be replaced." - }, + "NamePlaceholder": "Army Name", "Gear": { "Weapons": { "name" : "Magic Weapons", @@ -71,21 +17,15 @@ "rank0.name" : "Mundane Weapons", "rank1": { "name": "Magic Weapons", - "desc": "These weapons increase the army's Strike with that weapon by 1.", - "price": "20 RP", - "level": "Level 2;" + "desc": "These weapons increase the army's Strike with that weapon by 1." }, "rank2": { "name": "Greater Magic Weapons", - "desc": "These weapons increase the army's Strike with that weapon by 2.", - "price": "40 RP", - "level": "Level 10;" + "desc": "These weapons increase the army's Strike with that weapon by 2." }, "rank3": { "name": "Major Magic Weapons", - "desc": "These weapons increase the army's Strike with that weapon by 3.", - "price": "60 RP", - "level": "Level 16;" + "desc": "These weapons increase the army's Strike with that weapon by 3." } }, "Potions": { @@ -101,23 +41,53 @@ "rank0.name" : "Mundane Armor", "rank1": { "name": "Magic Armor", - "desc": "This armor increases the army's AC by 1.", - "price": "25 RP", - "level": "Level 5;" + "desc": "This armor increases the army's AC by 1." }, "rank2": { "name": "Greater Magic Armor", - "desc": "This armor increases the army's AC by 2.", - "price": "50 RP", - "level": "Level 11;" + "desc": "This armor increases the army's AC by 2." }, "rank3": { "name": "Major Magic Armor", - "desc": "This armor increases the army's AC by 3.", - "price": "75 RP", - "level": "Level 18;" + "desc": "This armor increases the army's AC by 3." } } + }, + "Potency": "Potency", + "Strikes": { + "melee": "Melee Strike", + "ranged": "Ranged Strike", + "Label": "Strikes", + "proficiency": "Proficiency", + "ammunitionLabel": "Ammunition: " + }, + + "routThreshold": "Rout Threshold", + "scouting": "Scouting", + "morale": "Morale", + "maneuver": "Maneuver", + "maxTactics": "Max Tactics", + "standardDC": "Standard DC", + "recruitmentDC": "Recruitment DC", + "ammunition": "Ammunition", + "ammunitionMax": "Max Ammunition", + "attributesHeader": "Attributes", + "routThresholdExceeded": "Army's HP is below Rout Threshold!", + "placeholderTacticsText": "Drag tactics here to add!", + "Tooltip": { + "tooltipPotion" : "Potions Info", + "tooltipRanged" : "Ranged Weapons Info", + "tooltipMelee" : "Melee Weapons Info", + "tooltipArmour" : "Armour Info", + "levelLabel": "Level: ", + "priceLabel": "Price: ", + "resourceLabel": " RP" + }, + "Potions": { + "label": "Potion(s)", + "drinkTooltip": "Use Potion (+1 HP)", + "drinkErrorNoPotions": "No potions available", + "drinkErrorFullHP": "HP is already full" } }, "Confirm": "Confirm", diff --git a/static/templates/actors/army/gear-card.hbs b/static/templates/actors/army/gear-card.hbs new file mode 100644 index 00000000000..88fcf6ebc51 --- /dev/null +++ b/static/templates/actors/army/gear-card.hbs @@ -0,0 +1,30 @@ +
+
+ +

{{name}}

+

{{level}}

+
+ +
+ {{#each traits as |trait|}} + {{localize trait.name}} + {{/each}} +
+ +
+ {{{description}}} +
+ + {{#each ranks as |rank|}} +
+
+
+

{{rank.name}}

+ {{{rank.level}}} + {{{rank.price}}} +
+ {{#if rank.desc}}{{rank.desc}}{{/if}} +
+ {{/each}} + {{#if price}}
{{{price}}}
{{/if}} +
diff --git a/static/templates/actors/army/sheet.hbs b/static/templates/actors/army/sheet.hbs new file mode 100644 index 00000000000..329fe7a86fe --- /dev/null +++ b/static/templates/actors/army/sheet.hbs @@ -0,0 +1,334 @@ +
+ {{!-- ============== --}} + {{!-- Header --}} + {{!-- ============== --}} +
+ {{!-- Portrait --}} +
+ +
+ +
+ + + {{!-- TRAITS --}} +
+ + + +
+ + +
+
+ + {{!-- ============== --}} + {{!-- SIDEBAR --}} + {{!-- ============== --}} + + + {{!-- ============== --}} + {{!-- Body --}} + {{!-- ============== --}} +
+
+  Description + {{editor data.details.description target="system.details.description" button=true owner=owner editable=editable placeholder="A description of this army!"}} +
+ + {{!-- ============== --}} + {{!-- Strikes --}} + {{!-- ============== --}} +
+
+  {{localize "PF2E.Kingmaker.Army.Strikes.Label"}} +
+ + + + + {{#if data.weapons.melee.unlocked}} + + {{else}} + + {{/if}} +
+
+ + + + + + + {{#if data.weapons.ranged.unlocked}} + + {{else}} + + {{/if}} +
+
+ +
+  {{localize "PF2E.Kingmaker.Army.Potency"}} +
+ + +
+
+ + +
+
+
+ +
+  Conditions +
    + {{#each document.itemTypes.effect as |effect|}} +
  1. + + + {{effect.name}} + + + {{#if (and (eq effect.type "effect") (eq effect.badge.type "counter"))}} + ({{coalesce effect.badge.label effect.badge.value}}) + {{/if}} + {{#if (eq effect.type "condition")}}{{#unless effect.active}} (Inactive){{/unless}}{{/if}} +
    + {{#if (and @root.options.editable (not effect.readonly))}} + {{#if (eq effect.badge.type "counter")}} + + + {{/if}} + + {{#if (eq effect.type "effect")}} + + {{/if}} + + {{else if effect.readonly}} + + {{/if}} +
    +
  2. + {{/each}} +
+
+ +
+  War Actions +
    + {{#each document.itemTypes.campaignFeature as |action|}} + {{#if (eq action.system.category "army-war-action")}} +
  1. + + + {{action.name}} + + + {{#if action.system.frequency}} +
    + + + / + {{action.system.frequency.max}} + {{localize "PF2E.Frequency.per"}} + {{localize (lookup @root.frequencies action.system.frequency.per)}} + +
    + {{/if}} +
    + + {{#if @root.options.editable}} + + + {{/if}} +
    +
  2. + {{/if}} + {{/each}} +
+
+ +
+  Tactics +
    + {{#each document.itemTypes.campaignFeature as |feat|}} + {{#if (eq feat.system.category "army-tactic")}} +
  1. + + + {{feat.name}} + + + {{#if feat.system.frequency}} +
    + + + / + {{feat.system.frequency.max}} + {{localize "PF2E.Frequency.per"}} + {{localize (lookup @root.frequencies feat.system.frequency.per)}} + +
    + {{/if}} +
    + + {{#if @root.options.editable}} + + + {{/if}} +
    +
  2. + {{/if}} + {{/each}} +
+
+
+