From 6a8e00d6e5c55ff48cad002cb465536151f47492 Mon Sep 17 00:00:00 2001 From: Shark that walks like a man Date: Sun, 26 Mar 2023 21:07:20 -0500 Subject: [PATCH] Move rest of XP macro to system codebase --- packs/data/macros.db/xp.json | 2 +- src/global.d.ts | 4 +- src/scripts/macros/index.ts | 2 +- src/scripts/macros/xp/dialog.ts | 204 ++++++++++++++++++++++ src/scripts/macros/{xp.ts => xp/index.ts} | 26 +-- src/scripts/set-game-pf2e.ts | 14 +- tests/module/xp.test.ts | 2 +- types/foundry/client/ui/dialog.d.ts | 2 +- 8 files changed, 237 insertions(+), 19 deletions(-) create mode 100644 src/scripts/macros/xp/dialog.ts rename src/scripts/macros/{xp.ts => xp/index.ts} (93%) diff --git a/packs/data/macros.db/xp.json b/packs/data/macros.db/xp.json index 0421ce42671..cfd30f6e52d 100644 --- a/packs/data/macros.db/xp.json +++ b/packs/data/macros.db/xp.json @@ -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 * @param actors {Array}\n * @param type {string}\n * @returns {Array}\n */\nfunction getHazardLevels(actors) {\n return actors.filter((a) => a.type === \"hazard\");\n}\n/**\n * @typedef {{data: {data: {details: {level: {value: number|string|undefined|null}}}, type: string}}} Actor\n */\n/**\n * @param actors {Array}\n * @param type {string}\n * @returns {Array}\n */\nfunction getLevels(actors, type) {\n return actors.filter((a) => a.type === type).map((a) => a.level ?? 1);\n}\n/**\n * @param xp {XP}\n * @returns {string}\n */\nfunction dialogTemplate(xp) {\n return `\n

XP

\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
${game.i18n.localize(\"PF2E.Encounter.Budget.PartySize\")}${xp.partySize}
${game.i18n.localize(\"PF2E.Encounter.Budget.PartyLevel\")}${xp.partyLevel}
${game.i18n.localize(\"PF2E.Encounter.Budget.Threat\")}${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.\"+xp.rating)} (${xp.totalXP} XP)
${game.i18n.localize(\"PF2E.Encounter.Budget.Reward\")}${xp.xpPerPlayer} XP
\n

${game.i18n.localize(\"PF2E.Encounter.Budget.EncounterBudget\")}

\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
${game.i18n.localize(\"PF2E.Encounter.Budget.Threat\")}${game.i18n.localize(\"PF2E.Encounter.Budget.XPBudget\")}${game.i18n.localize(\"PF2E.Encounter.Budget.XPNeeded\")}${game.i18n.localize(\"PF2E.Encounter.Budget.Reward\")}
${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.trivial\")}${xp.encounterBudgets.trivial}${xp.encounterBudgets.trivial - xp.totalXP}40
${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.low\")}${xp.encounterBudgets.low}${xp.encounterBudgets.low - xp.totalXP}60
${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.moderate\")}${xp.encounterBudgets.moderate}${xp.encounterBudgets.moderate - xp.totalXP}80
${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.severe\")}${xp.encounterBudgets.severe}${xp.encounterBudgets.severe - xp.totalXP}120
${game.i18n.localize(\"PF2E.Encounter.Budget.Threats.extreme\")}${xp.encounterBudgets.extreme}${xp.encounterBudgets.extreme - xp.totalXP}160
\n

${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureXPAndRole\")}

\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevel\")}XP${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.SuggestedRole\")}
${xp.partyLevel - 4}10${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-4\")}
${xp.partyLevel - 3}15${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-3\")}
${xp.partyLevel - 2}20${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-2\")}
${xp.partyLevel - 1}30${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-1\")}
${xp.partyLevel}40${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.0\")}
${xp.partyLevel + 1}60${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.1\")}
${xp.partyLevel + 2}80${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.2\")}
${xp.partyLevel + 3}120${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.3\")}
${xp.partyLevel + 4}160${game.i18n.localize(\"PF2E.Encounter.CreatureXPAndRole.CreatureLevels.4\")}
`;\n}\nconst askLevelPopupTemplate = () => {\n const partySize = parseInt(localStorage.getItem(\"xpMacroPartySize\") ?? 4, 10);\n const partyLevel = parseInt(localStorage.getItem(\"xpMacroPartyLevel\") ?? 1, 10);\n return `\n
\n
\n \n \n
\n
\n \n \n
\n
\n `;\n};\n/**\n * @param partyLevel {number}\n * @param partySize {number}\n * @param npcLevels {Array}\n * @param hazardLevels {Array}\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 * @param npcLevels {Array}\n * @param hazardLevels {Array}\n */\nfunction askPartyLevelAndSize(npcLevels, hazardLevels) {\n new Dialog({\n title: \"Party Information\",\n content: askLevelPopupTemplate,\n buttons: {\n no: {\n icon: '',\n label: \"Cancel\",\n },\n yes: {\n icon: '',\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}\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 if (pcLevels.length === 0) {\n askPartyLevelAndSize(npcLevels, hazardLevels);\n } else {\n showXP(pcLevels[0], pcLevels.length, npcLevels, hazardLevels);\n }\n}\nmain();", + "command": "game.pf2e.gm.xpFromEncounter();", "img": "systems/pf2e/icons/spells/athletic-rush.webp", "name": "XP", "ownership": { diff --git a/src/global.d.ts b/src/global.d.ts index 1e7f18079b4..7fd5a567b5f 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -40,6 +40,7 @@ import { rollActionMacro, rollItemMacro, stealthForSelected, + xpFromEncounter, } from "@scripts/macros/index.ts"; import { remigrate } from "@scripts/system/remigrate.ts"; import { CheckPF2e } from "@system/check/index.ts"; @@ -74,10 +75,11 @@ declare global { rollItemMacro: typeof rollItemMacro; gm: { calculateXP: typeof calculateXP; + editPersistent: typeof editPersistent; launchTravelSheet: typeof launchTravelSheet; perceptionForSelected: typeof perceptionForSelected; stealthForSelected: typeof stealthForSelected; - editPersistent: typeof editPersistent; + xpFromEncounter: typeof xpFromEncounter; }; system: { moduleArt: ModuleArt; diff --git a/src/scripts/macros/index.ts b/src/scripts/macros/index.ts index 141cab64b2f..a9e0d19b570 100644 --- a/src/scripts/macros/index.ts +++ b/src/scripts/macros/index.ts @@ -8,5 +8,5 @@ export { stealthForSelected } from "./stealth-for-selected-tokens.ts"; export { steelYourResolve } from "./steel-your-resolve.ts"; export { launchTravelSheet } from "./travel/index.ts"; export { treatWounds } from "./treat-wounds.ts"; -export { calculateXP } from "./xp.ts"; +export { calculateXP, xpFromEncounter } from "./xp/index.ts"; export { editPersistent } from "./edit-persistent.ts"; diff --git a/src/scripts/macros/xp/dialog.ts b/src/scripts/macros/xp/dialog.ts new file mode 100644 index 00000000000..aa500c756e4 --- /dev/null +++ b/src/scripts/macros/xp/dialog.ts @@ -0,0 +1,204 @@ +import { ActorPF2e, HazardPF2e } from "@actor"; +import { htmlQuery } from "@util"; +import type { XPCalculation } from "./index.ts"; + +function getHazards(actors: ActorPF2e[]): HazardPF2e[] { + return actors.filter((a): a is HazardPF2e => a.type === "hazard"); +} + +function getLevels(actors: ActorPF2e[], type: string): number[] { + return actors.filter((a) => a.type === type).map((a) => a.level); +} + +function dialogTemplate(xp: XPCalculation): string { + return ` +

XP

+ + + + + + + + + + + + + + + + + +
${game.i18n.localize("PF2E.Encounter.Budget.PartySize")}${xp.partySize}
${game.i18n.localize("PF2E.Encounter.Budget.PartyLevel")}${xp.partyLevel}
${game.i18n.localize("PF2E.Encounter.Budget.Threat")}${game.i18n.localize("PF2E.Encounter.Budget.Threats." + xp.rating)} (${xp.totalXP} XP)
${game.i18n.localize("PF2E.Encounter.Budget.Reward")}${xp.xpPerPlayer} XP
+

${game.i18n.localize("PF2E.Encounter.Budget.EncounterBudget")}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
${game.i18n.localize("PF2E.Encounter.Budget.Threat")}${game.i18n.localize("PF2E.Encounter.Budget.XPBudget")}${game.i18n.localize("PF2E.Encounter.Budget.XPNeeded")}${game.i18n.localize("PF2E.Encounter.Budget.Reward")}
${game.i18n.localize("PF2E.Encounter.Budget.Threats.trivial")}${xp.encounterBudgets.trivial}${xp.encounterBudgets.trivial - xp.totalXP}40
${game.i18n.localize("PF2E.Encounter.Budget.Threats.low")}${xp.encounterBudgets.low}${xp.encounterBudgets.low - xp.totalXP}60
${game.i18n.localize("PF2E.Encounter.Budget.Threats.moderate")}${xp.encounterBudgets.moderate}${xp.encounterBudgets.moderate - xp.totalXP}80
${game.i18n.localize("PF2E.Encounter.Budget.Threats.severe")}${xp.encounterBudgets.severe}${xp.encounterBudgets.severe - xp.totalXP}120
${game.i18n.localize("PF2E.Encounter.Budget.Threats.extreme")}${xp.encounterBudgets.extreme}${xp.encounterBudgets.extreme - xp.totalXP}160
+

${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureXPAndRole")}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevel")}XP${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.SuggestedRole")}
${xp.partyLevel - 4}10${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-4")}
${xp.partyLevel - 3}15${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-3")}
${xp.partyLevel - 2}20${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-2")}
${xp.partyLevel - 1}30${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.-1")}
${xp.partyLevel}40${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.0")}
${xp.partyLevel + 1}60${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.1")}
${xp.partyLevel + 2}80${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.2")}
${xp.partyLevel + 3}120${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.3")}
${xp.partyLevel + 4}160${game.i18n.localize("PF2E.Encounter.CreatureXPAndRole.CreatureLevels.4")}
`; +} + +const askLevelPopupTemplate = (): string => { + const partySize = Math.trunc(Number(localStorage.getItem("xpMacroPartySize") ?? 4)); + const partyLevel = Math.trunc(Number(localStorage.getItem("xpMacroPartyLevel") ?? 1)); + return ` +
+
+ + +
+
+ + +
+
+ `; +}; + +function showXP(partyLevel: number, partySize: number, npcLevels: number[], hazards: HazardPF2e[]): void { + const xp = game.pf2e.gm.calculateXP(partyLevel, partySize, npcLevels, hazards, { + proficiencyWithoutLevel: game.settings.get("pf2e", "proficiencyVariant") === "ProficiencyWithoutLevel", + }); + new Dialog({ + title: "XP", + content: dialogTemplate(xp), + buttons: {}, + }).render(true); +} + +function askPartyLevelAndSize(npcLevels: number[], hazards: HazardPF2e[]): void { + new Dialog({ + title: "Party Information", + content: askLevelPopupTemplate, + buttons: { + no: { + icon: '', + label: "Cancel", + }, + yes: { + icon: '', + label: "Calculate XP", + callback: ($html) => { + const html = $html[0]; + const partySize = Math.abs( + Math.trunc(Number(htmlQuery(html, "[name=party-size]")?.value || 1)) + ); + const partyLevel = Math.abs( + Math.trunc(Number(htmlQuery(html, "[name=party-level]")?.value || 1)) + ); + + // persist for future uses + localStorage.setItem("xpMacroPartySize", partySize.toString()); + localStorage.setItem("xpMacroPartyLevel", partyLevel.toString()); + showXP(partyLevel, partySize, npcLevels, hazards); + }, + }, + }, + default: "yes", + }).render(true); +} + +function xpFromEncounter(): void { + const actors = canvas.tokens.controlled.flatMap((a) => a.actor ?? []); + const npcLevels = getLevels(actors, "npc"); + const pcLevels = getLevels(actors, "character"); + const hazards = getHazards(actors); + if (npcLevels.length === 0 && hazards.length === 0) { + ui.notifications.error(`You must select at least one npc and/or hazard token and optionally all PC tokens`); + return; + } + if (pcLevels.length === 0) { + askPartyLevelAndSize(npcLevels, hazards); + } else { + showXP(pcLevels[0], pcLevels.length, npcLevels, hazards); + } +} + +export { xpFromEncounter }; diff --git a/src/scripts/macros/xp.ts b/src/scripts/macros/xp/index.ts similarity index 93% rename from src/scripts/macros/xp.ts rename to src/scripts/macros/xp/index.ts index a95ebcc83ad..dff3f90a012 100644 --- a/src/scripts/macros/xp.ts +++ b/src/scripts/macros/xp/index.ts @@ -1,8 +1,9 @@ +import { DCOptions } from "@module/dc.ts"; + /** * Rules are implemented as described in https://2e.aonprd.com/Rules.aspx?ID=575 * including the variant rules for proficiency without level https://2e.aonprd.com/Rules.aspx?ID=1371 */ -import { DCOptions } from "@module/dc.ts"; // level without proficiency variant const xpVariantCreatureDifferences = new Map([ @@ -65,12 +66,7 @@ function calculateCreatureXP(partyLevel: number, npcLevel: number, dcOptions: DC } } -interface HazardLevel { - level: number; - isComplex: boolean; -} - -function getHazardXp(partyLevel: number, hazard: HazardLevel, dcOptions: DCOptions): number { +function getHazardXp(partyLevel: number, hazard: HazardBrief, dcOptions: DCOptions): number { if (hazard.isComplex) { return calculateCreatureXP(partyLevel, hazard.level, dcOptions); } else { @@ -113,7 +109,7 @@ function calculateEncounterRating(challenge: number, budgets: EncounterBudgets): } } -interface XP { +interface XPCalculation { encounterBudgets: EncounterBudgets; rating: keyof EncounterBudgets; ratingXP: number; @@ -123,13 +119,18 @@ interface XP { partyLevel: number; } -export function calculateXP( +interface HazardBrief { + level: number; + isComplex: boolean; +} + +function calculateXP( partyLevel: number, partySize: number, npcLevels: number[], - hazards: HazardLevel[], + hazards: HazardBrief[], dcOptions: DCOptions -): XP { +): XPCalculation { const creatureChallenge = npcLevels .map((level) => calculateCreatureXP(partyLevel, level, dcOptions)) .reduce((a, b) => a + b, 0); @@ -150,3 +151,6 @@ export function calculateXP( xpPerPlayer: Math.floor((totalXP / partySize) * 4), }; } + +export { xpFromEncounter } from "./dialog.ts"; +export { XPCalculation, calculateXP }; diff --git a/src/scripts/set-game-pf2e.ts b/src/scripts/set-game-pf2e.ts index b4e356230cc..629fd16b827 100644 --- a/src/scripts/set-game-pf2e.ts +++ b/src/scripts/set-game-pf2e.ts @@ -1,5 +1,6 @@ +import { Action } from "@actor/actions/index.ts"; import { AutomaticBonusProgression } from "@actor/character/automatic-bonus-progression.ts"; -import { CheckModifier, ModifierPF2e, MODIFIER_TYPE, StatisticModifier } from "@actor/modifiers.ts"; +import { CheckModifier, MODIFIER_TYPE, ModifierPF2e, StatisticModifier } from "@actor/modifiers.ts"; import { CoinsPF2e } from "@item/physical/helpers.ts"; import { CompendiumBrowser } from "@module/apps/compendium-browser/index.ts"; import { EffectsPanel } from "@module/apps/effects-panel.ts"; @@ -31,7 +32,7 @@ import { EffectTracker } from "@system/effect-tracker.ts"; import { ModuleArt } from "@system/module-art.ts"; import { TextEditorPF2e } from "@system/text-editor.ts"; import { sluggify } from "@util"; -import { Action } from "@actor/actions/index.ts"; +import { xpFromEncounter } from "./macros/xp/dialog.ts"; /** Expose public game.pf2e interface */ export const SetGamePF2e = { @@ -69,7 +70,14 @@ export const SetGamePF2e = { actions, effectPanel: new EffectsPanel(), effectTracker: new EffectTracker(), - gm: { calculateXP, launchTravelSheet, perceptionForSelected, stealthForSelected, editPersistent }, + gm: { + calculateXP, + editPersistent, + launchTravelSheet, + perceptionForSelected, + stealthForSelected, + xpFromEncounter, + }, licenseViewer: new LicenseViewer(), rollActionMacro, rollItemMacro, diff --git a/tests/module/xp.test.ts b/tests/module/xp.test.ts index a55fd2607e8..653d58a4a08 100644 --- a/tests/module/xp.test.ts +++ b/tests/module/xp.test.ts @@ -1,4 +1,4 @@ -import { calculateXP } from "@scripts/macros/xp.ts"; +import { calculateXP } from "@scripts/macros/xp/index.ts"; const dcOptions = { proficiencyWithoutLevel: false, diff --git a/types/foundry/client/ui/dialog.d.ts b/types/foundry/client/ui/dialog.d.ts index a6bc8fd1ec8..88d47bcbda7 100644 --- a/types/foundry/client/ui/dialog.d.ts +++ b/types/foundry/client/ui/dialog.d.ts @@ -1,6 +1,6 @@ interface DialogData { title?: string; - content?: string | HTMLElement; + content?: string | HTMLElement | (() => string | HTMLElement); close?: (html: HTMLElement | JQuery) => void; buttons?: Record; default?: string;