Skip to content

Commit

Permalink
Add creature table to XP macro
Browse files Browse the repository at this point in the history
  • Loading branch information
blmaier authored and stwlam committed Nov 27, 2021
1 parent b6ffac9 commit b24bf13
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 12 deletions.
2 changes: 1 addition & 1 deletion packs/data/pf2e-macros.db/xp.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"_id": "MAHxEeGf31wqv3jp",
"author": "oxKN2HrOvz2sSqlF",
"command": "/**\n * @typedef {{data: {data: {details: {level: number|string|undefined|null, isComplex: boolean}}, type: string}}} Hazard\n */\n\n/**\n * @param actors {Array<Hazard>}\n * @param type {string}\n * @returns {Array<HazardLevel>}\n */\nfunction getHazardLevels(actors) {\n return actors.filter((a) => a.data.type === \"hazard\");\n}\n\n/**\n * @typedef {{data: {data: {details: {level: {value: number|string|undefined|null}}}, type: string}}} Actor\n */\n\n/**\n * @param actors {Array<Actor>}\n * @param type {string}\n * @returns {Array<number>}\n */\nfunction getLevels(actors, type) {\n return actors.filter((a) => a.data.type === type).map((a) => parseInt(a.data.data.details.level.value ?? \"1\", 10));\n}\n\n/**\n * @param xp {XP}\n * @returns {string}\n */\nfunction dialogTemplate(xp) {\n return `\n<h2>XP</h2>\n<table>\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.PartySize\")}</th>\n <td>${xp.partySize}</td>\n </tr>\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.PartyLevel\")}</th>\n <td>${xp.partyLevel}</td>\n </tr>\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.Threat\")}</th>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.\"+xp.rating)} (${xp.totalXP} XP)</td>\n </tr>\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.Reward\")}</th>\n <td>${xp.ratingXP} XP</td>\n </tr>\n</table>\n<h2>${game.i18n.localize(\"PF2E.Encounter.Budget.EncounterBudget\")}</h2>\n<table>\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.Threat\")}</th>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.XPBudget\")}</th>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.Reward\")}</th>\n </tr>\n <tr>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.trivial\")}</td>\n <td>${xp.encounterBudgets.trivial} XP</td>\n <td>40 XP</td>\n </tr>\n <tr>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.low\")}</td>\n <td>${xp.encounterBudgets.low} XP</td>\n <td>60 XP</td>\n </tr>\n <tr>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.moderate\")}</td>\n <td>${xp.encounterBudgets.moderate} XP</td>\n <td>80 XP</td>\n </tr>\n <tr>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.severe\")}</td>\n <td>${xp.encounterBudgets.severe} XP</td>\n <td>120 XP</td>\n </tr>\n <tr>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.extreme\")}</td>\n <td>${xp.encounterBudgets.extreme} XP</td>\n <td>160 XP</td>\n </tr>\n</table>`;\n}\n\nconst askLevelPopupTemplate = () => {\n const partySize = parseInt(localStorage.getItem(\"xpMacroPartySize\") ?? 4, 10);\n const partyLevel = parseInt(localStorage.getItem(\"xpMacroPartyLevel\") ?? 1, 10);\n return `\n <form>\n <div class=\"form-group\">\n <label>${game.i18n.localize(\"PF2E.Encounter.Budget.PartySize\")}</label>\n <input id=\"party-size\" name=\"party-size\" type=\"number\" value=\"${partySize}\">\n </div>\n <div class=\"form-group\">\n <label>${game.i18n.localize(\"PF2E.Encounter.Budget.PartyLevel\")}</label>\n <input id=\"party-level\" name=\"party-level\" type=\"number\" value=\"${partyLevel}\">\n </div>\n </form>\n `;\n};\n\n/**\n * @param partyLevel {number}\n * @param partySize {number}\n * @param npcLevels {Array<number>}\n * @param hazardLevels {Array<HazardLevel>}\n */\nfunction showXP(partyLevel, partySize, npcLevels, hazardLevels) {\n const xp = game.pf2e.gm.calculateXP(partyLevel, partySize, npcLevels, hazardLevels, {\n proficiencyWithoutLevel: game.settings.get(\"pf2e\", \"proficiencyVariant\") === \"ProficiencyWithoutLevel\",\n });\n new Dialog({\n title: \"XP\",\n content: dialogTemplate(xp),\n buttons: {},\n }).render(true);\n}\n\n/**\n * @param npcLevels {Array<number>}\n * @param hazardLevels {Array<HazardLevel>}\n */\nfunction askPartyLevelAndSize(npcLevels, hazardLevels) {\n new Dialog({\n title: \"Party Information\",\n content: askLevelPopupTemplate,\n buttons: {\n no: {\n icon: '<i class=\"fas fa-times\"></i>',\n label: \"Cancel\",\n },\n yes: {\n icon: '<i class=\"fas fa-calculator\"></i>',\n label: \"Calculate XP\",\n callback: ($html) => {\n const partySize = parseInt($html[0].querySelector('[name=\"party-size\"]').value, 10) ?? 1;\n const partyLevel = parseInt($html[0].querySelector('[name=\"party-level\"]').value, 10) ?? 1;\n // persist for future uses\n localStorage.setItem(\"xpMacroPartySize\", partySize);\n localStorage.setItem(\"xpMacroPartyLevel\", partyLevel);\n showXP(partyLevel, partySize, npcLevels, hazardLevels);\n },\n },\n },\n default: \"yes\",\n }).render(true);\n}\n\nfunction main() {\n const actors = canvas.tokens.controlled.map((a) => a.actor);\n const npcLevels = getLevels(actors, \"npc\");\n const pcLevels = getLevels(actors, \"character\");\n const hazardLevels = getHazardLevels(actors);\n if (npcLevels.length === 0 && hazardLevels.length === 0) {\n ui.notifications.error(`You must select at least one npc and/or hazard token and optionally all PC tokens`);\n return;\n }\n\n if (pcLevels.length === 0) {\n askPartyLevelAndSize(npcLevels, hazardLevels);\n } else {\n showXP(pcLevels[0], pcLevels.length, npcLevels, hazardLevels);\n }\n}\n\nmain();",
"command": "/**\n * @typedef {{data: {data: {details: {level: number|string|undefined|null, isComplex: boolean}}, type: string}}} Hazard\n */\n\n/**\n * @param actors {Array<Hazard>}\n * @param type {string}\n * @returns {Array<HazardLevel>}\n */\nfunction getHazardLevels(actors) {\n return actors.filter((a) => a.data.type === \"hazard\");\n}\n\n/**\n * @typedef {{data: {data: {details: {level: {value: number|string|undefined|null}}}, type: string}}} Actor\n */\n\n/**\n * @param actors {Array<Actor>}\n * @param type {string}\n * @returns {Array<number>}\n */\nfunction getLevels(actors, type) {\n return actors.filter((a) => a.data.type === type).map((a) => parseInt(a.data.data.details.level.value ?? \"1\", 10));\n}\n\n/**\n * @param xp {XP}\n * @returns {string}\n */\nfunction dialogTemplate(xp) {\n return `\n<h2>XP</h2>\n<table>\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.PartySize\")}</th>\n <td>${xp.partySize}</td>\n </tr>\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.PartyLevel\")}</th>\n <td>${xp.partyLevel}</td>\n </tr>\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.Threat\")}</th>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.\"+xp.rating)} (${xp.totalXP} XP)</td>\n </tr>\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.Reward\")}</th>\n <td>${xp.ratingXP} XP</td>\n </tr>\n</table>\n<h2>${game.i18n.localize(\"PF2E.Encounter.Budget.EncounterBudget\")}</h2>\n<table class=\"pf2-table\">\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.Threat\")}</th>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.XPBudget\")}</th>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.XPNeeded\")}</th>\n <th>${game.i18n.localize(\"PF2E.Encounter.Budget.Reward\")}</th>\n </tr>\n <tr>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.trivial\")}</td>\n <td>${xp.encounterBudgets.trivial}</td>\n <td>${xp.encounterBudgets.trivial - xp.totalXP}</td>\n <td>40</td>\n </tr>\n <tr>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.low\")}</td>\n <td>${xp.encounterBudgets.low}</td>\n <td>${xp.encounterBudgets.low - xp.totalXP}</td>\n <td>60</td>\n </tr>\n <tr>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.moderate\")}</td>\n <td>${xp.encounterBudgets.moderate}</td>\n <td>${xp.encounterBudgets.moderate - xp.totalXP}</td>\n <td>80</td>\n </tr>\n <tr>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.severe\")}</td>\n <td>${xp.encounterBudgets.severe}</td>\n <td>${xp.encounterBudgets.severe - xp.totalXP}</td>\n <td>120</td>\n </tr>\n <tr>\n <td>${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.extreme\")}</td>\n <td>${xp.encounterBudgets.extreme}</td>\n <td>${xp.encounterBudgets.extreme - xp.totalXP}</td>\n <td>160</td>\n </tr>\n</table>\n<h2>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureXPAndRole\")}</h2>\n<table class=\"pf2-table\">\n <tr>\n <th>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevel\")}</th>\n <th>XP</th>\n <th>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.SuggestedRole\")}</th>\n </tr>\n <tr>\n <td>${xp.partyLevel - 4}</td>\n <td>10</td>\n <td>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-4\")}</td>\n </tr>\n <tr>\n <td>${xp.partyLevel - 3}</td>\n <td>15</td>\n <td>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-3\")}</td>\n </tr>\n <tr>\n <td>${xp.partyLevel - 2}</td>\n <td>20</td>\n <td>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-2\")}</td>\n </tr>\n <tr>\n <td>${xp.partyLevel - 1}</td>\n <td>30</td>\n <td>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-1\")}</td>\n </tr>\n <tr>\n <td>${xp.partyLevel}</td>\n <td>40</td>\n <td>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.0\")}</td>\n </tr>\n <tr>\n <td>${xp.partyLevel + 1}</td>\n <td>60</td>\n <td>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.1\")}</td>\n </tr>\n <tr>\n <td>${xp.partyLevel + 2}</td>\n <td>80</td>\n <td>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.2\")}</td>\n </tr>\n <tr>\n <td>${xp.partyLevel + 3}</td>\n <td>120</td>\n <td>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.3\")}</td>\n </tr>\n <tr>\n <td>${xp.partyLevel + 4}</td>\n <td>160</td>\n <td>${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.4\")}</td>\n </tr>\n</table>`;\n}\n\nconst askLevelPopupTemplate = () => {\n const partySize = parseInt(localStorage.getItem(\"xpMacroPartySize\") ?? 4, 10);\n const partyLevel = parseInt(localStorage.getItem(\"xpMacroPartyLevel\") ?? 1, 10);\n return `\n <form>\n <div class=\"form-group\">\n <label>${game.i18n.localize(\"PF2E.Encounter.Budget.PartySize\")}</label>\n <input id=\"party-size\" name=\"party-size\" type=\"number\" value=\"${partySize}\">\n </div>\n <div class=\"form-group\">\n <label>${game.i18n.localize(\"PF2E.Encounter.Budget.PartyLevel\")}</label>\n <input id=\"party-level\" name=\"party-level\" type=\"number\" value=\"${partyLevel}\">\n </div>\n </form>\n `;\n};\n\n/**\n * @param partyLevel {number}\n * @param partySize {number}\n * @param npcLevels {Array<number>}\n * @param hazardLevels {Array<HazardLevel>}\n */\nfunction showXP(partyLevel, partySize, npcLevels, hazardLevels) {\n const xp = game.pf2e.gm.calculateXP(partyLevel, partySize, npcLevels, hazardLevels, {\n proficiencyWithoutLevel: game.settings.get(\"pf2e\", \"proficiencyVariant\") === \"ProficiencyWithoutLevel\",\n });\n new Dialog({\n title: \"XP\",\n content: dialogTemplate(xp),\n buttons: {},\n }).render(true);\n}\n\n/**\n * @param npcLevels {Array<number>}\n * @param hazardLevels {Array<HazardLevel>}\n */\nfunction askPartyLevelAndSize(npcLevels, hazardLevels) {\n new Dialog({\n title: \"Party Information\",\n content: askLevelPopupTemplate,\n buttons: {\n no: {\n icon: '<i class=\"fas fa-times\"></i>',\n label: \"Cancel\",\n },\n yes: {\n icon: '<i class=\"fas fa-calculator\"></i>',\n label: \"Calculate XP\",\n callback: ($html) => {\n const partySize = parseInt($html[0].querySelector('[name=\"party-size\"]').value, 10) ?? 1;\n const partyLevel = parseInt($html[0].querySelector('[name=\"party-level\"]').value, 10) ?? 1;\n // persist for future uses\n localStorage.setItem(\"xpMacroPartySize\", partySize);\n localStorage.setItem(\"xpMacroPartyLevel\", partyLevel);\n showXP(partyLevel, partySize, npcLevels, hazardLevels);\n },\n },\n },\n default: \"yes\",\n }).render(true);\n}\n\nfunction main() {\n const actors = canvas.tokens.controlled.map((a) => a.actor);\n const npcLevels = getLevels(actors, \"npc\");\n const pcLevels = getLevels(actors, \"character\");\n const hazardLevels = getHazardLevels(actors);\n if (npcLevels.length === 0 && hazardLevels.length === 0) {\n ui.notifications.error(`You must select at least one npc and/or hazard token and optionally all PC tokens`);\n return;\n }\n\n if (pcLevels.length === 0) {\n askPartyLevelAndSize(npcLevels, hazardLevels);\n } else {\n showXP(pcLevels[0], pcLevels.length, npcLevels, hazardLevels);\n }\n}\n\nmain();",
"flags": {},
"img": "systems/pf2e/icons/spells/athletic-rush.webp",
"name": "XP",
Expand Down
17 changes: 17 additions & 0 deletions static/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,7 @@
"Threat": "Threat",
"Reward": "Reward",
"XPBudget": "XP Budget",
"XPNeeded": "XP Needed",
"Threats": {
"trivial": "Trivial",
"low": "Low",
Expand All @@ -1042,6 +1043,22 @@
"extreme": "Extreme"
}
},
"CreatureXPAndRole": {
"CreatureXPAndRole": "Creature XP and Role",
"CreatureLevel": "Creature Level",
"SuggestedRole": "Suggested Role",
"CreatureLevels": {
"-4": "Low-threat lackey",
"-3": "Low- or moderate-threat lackey",
"-2": "Any lackey or standard creature",
"-1": "Any standard creature",
"0": "Any standard creature or low-threat boss",
"1": "Low- or moderate-threat boss",
"2": "Moderate- or severe-threat boss",
"3": "Severe- or extreme-threat boss",
"4": "Extreme-threat solo boss"
}
},
"ExcludingFromInitiative": "Excluding {type} {actor} from initiative.",
"HasNoInitiativeScore": "{actor} has no initiative score.",
"NoActiveEncounter": "There is no active encounter.",
Expand Down
81 changes: 70 additions & 11 deletions static/macros/xp.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,36 +50,95 @@ function dialogTemplate(xp) {
</tr>
</table>
<h2>${game.i18n.localize("PF2E.Encounter.Budget.EncounterBudget")}</h2>
<table>
<table class="pf2-table">
<tr>
<th>${game.i18n.localize("PF2E.Encounter.Budget.Threat")}</th>
<th>${game.i18n.localize("PF2E.Encounter.Budget.XPBudget")}</th>
<th>${game.i18n.localize("PF2E.Encounter.Budget.XPNeeded")}</th>
<th>${game.i18n.localize("PF2E.Encounter.Budget.Reward")}</th>
</tr>
<tr>
<td>${game.i18n.localize("PF2E.Encounter.Budget.Threats.trivial")}</td>
<td>${xp.encounterBudgets.trivial} XP</td>
<td>40 XP</td>
<td>${xp.encounterBudgets.trivial}</td>
<td>${xp.encounterBudgets.trivial - xp.totalXP}</td>
<td>40</td>
</tr>
<tr>
<td>${game.i18n.localize("PF2E.Encounter.Budget.Threats.low")}</td>
<td>${xp.encounterBudgets.low} XP</td>
<td>60 XP</td>
<td>${xp.encounterBudgets.low}</td>
<td>${xp.encounterBudgets.low - xp.totalXP}</td>
<td>60</td>
</tr>
<tr>
<td>${game.i18n.localize("PF2E.Encounter.Budget.Threats.moderate")}</td>
<td>${xp.encounterBudgets.moderate} XP</td>
<td>80 XP</td>
<td>${xp.encounterBudgets.moderate}</td>
<td>${xp.encounterBudgets.moderate - xp.totalXP}</td>
<td>80</td>
</tr>
<tr>
<td>${game.i18n.localize("PF2E.Encounter.Budget.Threats.severe")}</td>
<td>${xp.encounterBudgets.severe} XP</td>
<td>120 XP</td>
<td>${xp.encounterBudgets.severe}</td>
<td>${xp.encounterBudgets.severe - xp.totalXP}</td>
<td>120</td>
</tr>
<tr>
<td>${game.i18n.localize("PF2E.Encounter.Budget.Threats.extreme")}</td>
<td>${xp.encounterBudgets.extreme} XP</td>
<td>160 XP</td>
<td>${xp.encounterBudgets.extreme}</td>
<td>${xp.encounterBudgets.extreme - xp.totalXP}</td>
<td>160</td>
</tr>
</table>
<h2>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureXPAndRole")}</h2>
<table class="pf2-table">
<tr>
<th>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevel")}</th>
<th>XP</th>
<th>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.SuggestedRole")}</th>
</tr>
<tr>
<td>${xp.partyLevel - 4}</td>
<td>10</td>
<td>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-4")}</td>
</tr>
<tr>
<td>${xp.partyLevel - 3}</td>
<td>15</td>
<td>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-3")}</td>
</tr>
<tr>
<td>${xp.partyLevel - 2}</td>
<td>20</td>
<td>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-2")}</td>
</tr>
<tr>
<td>${xp.partyLevel - 1}</td>
<td>30</td>
<td>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-1")}</td>
</tr>
<tr>
<td>${xp.partyLevel}</td>
<td>40</td>
<td>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.0")}</td>
</tr>
<tr>
<td>${xp.partyLevel + 1}</td>
<td>60</td>
<td>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.1")}</td>
</tr>
<tr>
<td>${xp.partyLevel + 2}</td>
<td>80</td>
<td>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.2")}</td>
</tr>
<tr>
<td>${xp.partyLevel + 3}</td>
<td>120</td>
<td>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.3")}</td>
</tr>
<tr>
<td>${xp.partyLevel + 4}</td>
<td>160</td>
<td>${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.4")}</td>
</tr>
</table>`;
}
Expand Down

0 comments on commit b24bf13

Please sign in to comment.