Files
FoundryVTT/src/foundryvtt-pathfinder1-v10.8/pf1.mjs
2025-11-06 14:04:48 +01:00

906 lines
28 KiB
JavaScript

/**
* The core API provided by the system, available via the global `pf1`.
*
* @module
*/
// Imports for side effects
import "./less/pf1.less";
import "./module/hmr.mjs";
import "./module/patch-core.mjs";
import "module/compendium-directory.mjs";
import "./module/chatlog.mjs";
// Import Modules
import { measureDistances } from "./module/utils/canvas.mjs";
import { moduleToObject, setDefaultSceneScaling } from "./module/utils/lib.mjs";
import { initializeSocket } from "./module/socket.mjs";
import { SemanticVersion } from "./module/utils/semver.mjs";
import * as macros from "./module/documents/macros.mjs";
import * as chatUtils from "./module/utils/chat.mjs";
import { initializeModuleIntegration } from "./module/modules.mjs";
import { ActorPFProxy } from "@actor/actor-proxy.mjs";
import { ItemPFProxy } from "@item/item-proxy.mjs";
// New API
import * as PF1 from "./module/config.mjs";
import * as PF1CONST from "./module/const.mjs";
import * as applications from "./module/applications/_module.mjs";
import * as documents from "./module/documents/_module.mjs";
import * as actionUse from "./module/action-use/_module.mjs";
import * as chat from "./module/chat/_module.mjs";
import * as _canvas from "./module/canvas/_module.mjs";
import * as dice from "./module/dice/_module.mjs";
import * as components from "./module/components/_module.mjs";
import * as utils from "./module/utils/_module.mjs";
import * as registry from "./module/registry/_module.mjs";
import * as migrations from "./module/migration.mjs";
import * as rollFunctions from "./module/utils/roll-functions.mjs";
// ESM exports, to be kept in sync with globalThis.pf1
export {
actionUse,
applications,
_canvas as canvas,
components,
PF1 as config,
PF1CONST as const,
dice,
documents,
migrations,
registry,
utils,
chat,
};
globalThis.pf1 = moduleToObject({
actionUse,
applications,
canvas: _canvas,
components,
config: PF1,
const: PF1CONST,
dice,
documents,
migrations,
registry,
/** @type {TooltipPF|null} */
tooltip: null,
utils,
chat,
// Initialize skip confirm prompt value
skipConfirmPrompt: false,
});
/* -------------------------------------------- */
/* Foundry VTT Initialization */
/* -------------------------------------------- */
Hooks.once("init", function () {
console.log(`PF1 | Initializing Pathfinder 1 System`);
// Redirect notifications to console before Notifications is ready
ui.notifications = {
info: (msg, opts = {}) => (opts.console !== false ? console.log(msg) : undefined),
warn: (msg, opts = {}) => (opts.console !== false ? console.warn(msg) : undefined),
error: (msg, opts = {}) => (opts.console !== false ? console.error(msg) : undefined),
};
// Global exports
globalThis.RollPF = dice.RollPF;
// Record Configuration Values
CONFIG.PF1 = pf1.config;
// Canvas object classes and configuration
CONFIG.Canvas.layers.templates.layerClass = _canvas.TemplateLayerPF;
CONFIG.MeasuredTemplate.objectClass = _canvas.MeasuredTemplatePF;
CONFIG.MeasuredTemplate.defaults.originalAngle = CONFIG.MeasuredTemplate.defaults.angle;
CONFIG.MeasuredTemplate.defaults.angle = 90; // PF1 uses 90 degree angles
CONFIG.Token.objectClass = _canvas.TokenPF;
// Document classes
CONFIG.Actor.documentClass = ActorPFProxy;
CONFIG.Actor.documentClasses = {
character: documents.actor.ActorCharacterPF,
npc: documents.actor.ActorNPCPF,
haunt: documents.actor.ActorHauntPF,
trap: documents.actor.ActorTrapPF,
vehicle: documents.actor.ActorVehiclePF,
basic: documents.actor.BasicActorPF,
};
CONFIG.Item.documentClass = ItemPFProxy;
CONFIG.Item.documentClasses = {
attack: documents.item.ItemAttackPF,
buff: documents.item.ItemBuffPF,
class: documents.item.ItemClassPF,
consumable: documents.item.ItemConsumablePF,
container: documents.item.ItemContainerPF,
equipment: documents.item.ItemEquipmentPF,
feat: documents.item.ItemFeatPF,
loot: documents.item.ItemLootPF,
race: documents.item.ItemRacePF,
spell: documents.item.ItemSpellPF,
weapon: documents.item.ItemWeaponPF,
implant: documents.item.ItemImplantPF,
};
CONFIG.Token.documentClass = documents.TokenDocumentPF;
CONFIG.ActiveEffect.documentClass = documents.ActiveEffectPF;
CONFIG.ActiveEffect.legacyTransferral = false; // TODO: Remove once legacy transferral is no longer default.
CONFIG.Combat.documentClass = documents.CombatPF;
CONFIG.Combatant.documentClass = documents.CombatantPF;
CONFIG.ChatMessage.documentClass = documents.ChatMessagePF;
// UI classes
CONFIG.ui.items = applications.ItemDirectoryPF;
// Dice config
CONFIG.Dice.rolls.unshift(dice.RollPF);
for (const [key, term] of Object.entries(dice.terms.fn)) {
CONFIG.Dice.termTypes[key] = term;
}
for (const [key, term] of Object.entries(dice.terms.aux)) {
CONFIG.Dice.termTypes[key] = term;
}
CONFIG.Dice.rolls.push(dice.D20RollPF);
CONFIG.Dice.rolls.push(dice.DamageRoll);
Object.defineProperties(CONFIG.Dice, {
RollPF: {
get() {
foundry.utils.logCompatibilityWarning(
"CONFIG.Dice.RollPF is deprecated in favor of RollPF global and pf1.dice.RollPF",
{ since: "PF1 v10", until: "PF1 v11" }
);
return pf1.dice.RollPF;
},
},
});
Object.defineProperties(CONFIG.Dice.rolls, {
DamageRoll: {
get() {
foundry.utils.logCompatibilityWarning(
"CONFIG.Dice.rolls.DamageRoll is deprecated in favor of pf1.dice.DamageRoll",
{ since: "PF1 v10", until: "PF1 v11" }
);
return pf1.dice.DamageRoll;
},
},
D20RollPF: {
get() {
foundry.utils.logCompatibilityWarning(
"CONFIG.Dice.rolls.D20RollPF is deprecated in favor of pf1.dice.D20RollPF",
{ since: "PF1 v10", until: "PF1 v11" }
);
return pf1.dice.D20RollPF;
},
},
});
// Modifier -> Type
Object.defineProperties(pf1.config, {
bonusModifiers: {
get() {
foundry.utils.logCompatibilityWarning(
"pf1.config.bonusModifiers is deprecated in favor of pf1.config.bonusTypes",
{ since: "PF1 v10", until: "PF1 v11" }
);
return this.bonusTypes;
},
},
stackingBonusModifiers: {
get() {
foundry.utils.logCompatibilityWarning(
"pf1.config.stackingBonusModifiers is deprecated in favor of pf1.config.stackingBonusTypes",
{ since: "PF1 v10", until: "PF1 v11" }
);
return this.stackingBonusTypes;
},
},
});
Object.defineProperty(pf1.config, "itemTypes", {
get() {
foundry.utils.logCompatibilityWarning("pf1.config.itemTypes is deprecated in favor of CONFIG.Item.typeLabels", {
since: "PF1 v10",
until: "PF1 v11",
});
return Object.fromEntries(
Object.entries(CONFIG.Item.typeLabels).map(([key, label]) => [key, game.i18n.localize(label)])
);
},
});
Object.defineProperty(pf1.utils, "rollPreProcess", {
get() {
foundry.utils.logCompatibilityWarning("pf1.utils.rollPreProcess.* is deprecated in favor of pf1.utils.roll.*", {
since: "PF1 v10",
until: "PF1 v11",
});
return pf1.utils.roll;
},
});
Object.defineProperty(pf1.applications, "ActionChooser", {
get() {
foundry.utils.logCompatibilityWarning(
"pf1.utils.ActionChooser is deprecated in favor of pf1.utils.ActionSelector",
{
since: "PF1 v10",
until: "PF1 v11",
}
);
return pf1.applications.ActionSelector;
},
});
CONFIG.time.roundTime = 6;
// Register System Settings
documents.settings.registerSystemSettings();
documents.settings.registerClientSettings();
setDefaultSceneScaling();
// Preload Handlebars Templates
utils.handlebars.preloadHandlebarsTemplates();
utils.handlebars.registerHandlebarsHelpers();
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet("pf1", applications.actor.ActorSheetPFCharacter, {
label: "PF1.Sheet.PC",
types: ["character"],
makeDefault: true,
});
Actors.registerSheet("pf1", applications.actor.ActorSheetPFNPC, {
label: "PF1.Sheet.NPC",
types: ["npc"],
makeDefault: true,
});
Actors.registerSheet("pf1", applications.actor.ActorSheetPFNPCLite, {
label: "PF1.Sheet.NPCLite",
types: ["npc"],
makeDefault: false,
});
Actors.registerSheet("pf1", applications.actor.ActorSheetPFNPCLoot, {
label: "PF1.Sheet.NPCLoot",
types: ["npc"],
makeDefault: false,
});
Actors.registerSheet("pf1", applications.actor.ActorSheetPFHaunt, {
label: "PF1.Sheet.Haunt",
types: ["haunt"],
makeDefault: true,
});
Actors.registerSheet("pf1", applications.actor.ActorSheetPFTrap, {
label: "PF1.Sheet.Trap",
types: ["trap"],
makeDefault: true,
});
Actors.registerSheet("pf1", applications.actor.ActorSheetPFVehicle, {
label: "PF1.Sheet.Vehicle",
types: ["vehicle"],
makeDefault: true,
});
Actors.registerSheet("pf1", applications.actor.ActorSheetPFBasic, {
label: "PF1.Sheet.Basic",
types: ["basic"],
makeDefault: true,
});
Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("pf1", applications.item.ItemSheetPF, {
label: "PF1.Sheet.Item",
types: ["class", "feat", "spell", "consumable", "equipment", "loot", "weapon", "buff", "attack", "race", "implant"],
makeDefault: true,
});
Items.registerSheet("pf1", applications.item.ItemSheetPF_Container, {
label: "PF1.Sheet.Container",
types: ["container"],
makeDefault: true,
});
// Register detection modes
for (const mode of Object.values(pf1.canvas.detectionModes)) {
CONFIG.Canvas.detectionModes[mode.ID] = new mode({
id: mode.ID,
label: mode.LABEL,
type: mode.DETECTION_TYPE ?? DetectionMode.DETECTION_TYPES.SIGHT,
});
}
// Register vision modes
CONFIG.Canvas.visionModes.darkvision = pf1.canvas.visionModes.darkvision;
// Initialize socket listener
initializeSocket();
// Initialize module integrations
initializeModuleIntegration();
// Initialize registries with initial/built-in data
const registries = /** @type {const} */ ([
["damageTypes", registry.DamageTypes],
["materialTypes", registry.MaterialTypes],
["scriptCalls", registry.ScriptCalls],
["conditions", registry.Conditions],
["sources", registry.Sources],
]);
for (const [registryName, registryClass] of registries) {
pf1.registry[registryName] = new registryClass();
}
//Calculate conditions for world
CONFIG.statusEffects = pf1.utils.init.getConditions();
Object.defineProperty(pf1.config, "conditions", {
get: () => {
foundry.utils.logCompatibilityWarning(
"Conditions have been moved into the Conditions registry. " +
"Use pf1.registry.conditions.getLabels() for the old format, or access the collection for full condition data.",
{ since: "PF1 v10", until: "PF1 v11" }
);
return pf1.registry.conditions.getLabels();
},
});
Object.defineProperty(pf1.config, "conditionTextures", {
get: () => {
foundry.utils.logCompatibilityWarning(
"Condition textures have been moved into the Conditions registry. " +
"Access the collection for full condition data.",
{ since: "PF1 v10", until: "PF1 v11" }
);
return Object.fromEntries(
pf1.registry.conditions.map((registryObject) => [registryObject.id, registryObject.texture])
);
},
});
Object.defineProperty(pf1.config, "conditionMechanics", {
get: () => {
foundry.utils.logCompatibilityWarning(
"Condition mechanics have been moved into the Conditions registry. " +
"Access the collection for full condition data.",
{ since: "PF1 v10", until: "PF1 v11" }
);
return Object.fromEntries(
pf1.registry.conditions.map((registryObject) => [registryObject.id, registryObject.mechanics])
);
},
});
// Diagonal ruleset implementation
SquareGrid.prototype.measureDistances = measureDistances;
// Call post-init hook
Hooks.callAll("pf1PostInit");
});
// Load Quench test in development environment
if (import.meta.env.DEV) {
await import("./module/test/index.mjs");
}
/* -------------------------------------------- */
/* Foundry VTT Setup */
/* -------------------------------------------- */
// Pre-translation passes
Hooks.once("i18nInit", function () {
// Localize pf1.config objects once up-front
const toLocalize = [
"abilities",
"abilitiesShort",
"alignments",
"alignmentsShort",
"currencies",
"distanceUnits",
"itemActionTypes",
"senses",
"skills",
"timePeriods",
"timePeriodsShort",
"durationEndEvents",
"savingThrows",
"ac",
"featTypes",
"featTypesPlurals",
"traitTypes",
"racialTraitCategories",
"raceTypes",
"conditionTypes",
"lootTypes",
"flyManeuverabilities",
"favouredClassBonuses",
"abilityTypes",
"weaponGroups",
"weaponTypes",
"weaponProperties",
"spellComponents",
"spellDescriptors",
"spellSchools",
"spellLevels",
"spellcasting",
"armorProficiencies",
"weaponProficiencies",
"actorSizes",
"abilityActivationTypes",
"abilityActivationTypesPlurals",
"limitedUsePeriods",
"equipmentTypes",
"equipmentSlots",
"implantSlots",
"implantTypes",
"consumableTypes",
"attackTypes",
"buffTypes",
"divineFocus",
"classSavingThrows",
"classBAB",
"classTypes",
"measureTemplateTypes",
"creatureTypes",
"measureUnits",
"measureUnitsShort",
"languages",
"weaponHoldTypes",
"auraStrengths",
"conditionalTargets",
"bonusTypes",
"abilityActivationTypes_unchained",
"abilityActivationTypesPlurals_unchained",
"actorStatures",
"ammoTypes",
"damageResistances",
"vehicles",
"woundThresholdConditions",
];
// Localize pf1.const objects
const toLocalizeConst = ["messageVisibility"];
// Config (sub-)objects to be sorted
const toSort = [
"bonusTypes",
"skills",
"traitTypes",
"racialTraitCategories",
"conditionTypes",
"consumableTypes",
"creatureTypes",
"featTypes",
"weaponProperties",
"spellSchools",
"languages",
];
/**
* Helper function to recursively localize object entries
*
* @param {object} obj - The object to be localized
* @param {string} cat - The object's name
* @returns {object} The localized object
*/
const doLocalize = (obj, cat) => {
// Create tuples of (key, localized object/string)
const localized = Object.entries(obj).reduce((arr, [key, value]) => {
if (typeof value === "string") arr.push([key, game.i18n.localize(value)]);
else if (typeof value === "object") arr.push([key, doLocalize(value, `${cat}.${key}`)]);
return arr;
}, []);
if (toSort.includes(cat)) {
// Sort simple strings, fall back to sorting by label for objects/categories
localized.sort(([akey, aval], [bkey, bval]) => {
// Move misc to bottom of every list
if (akey === "misc") return 1;
else if (bkey === "misc") return -1;
// Regular sorting of localized strings
const localA = typeof aval === "string" ? aval : aval._label;
const localB = typeof bval === "string" ? bval : bval._label;
return localA.localeCompare(localB);
});
}
// Get the localized and sorted object out of tuple
return localized.reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
};
const doLocalizePaths = (obj, paths = []) => {
for (const path of paths) {
const value = foundry.utils.getProperty(obj, path);
if (value) {
foundry.utils.setProperty(obj, path, game.i18n.localize(value));
}
}
};
const doLocalizeKeys = (obj, keys = []) => {
for (const path of Object.keys(foundry.utils.flattenObject(obj))) {
const key = path.split(".").at(-1);
if (keys.includes(key)) {
const value = foundry.utils.getProperty(obj, path);
if (value) {
foundry.utils.setProperty(obj, path, game.i18n.localize(value));
}
}
}
};
// Localize and sort CONFIG objects
for (const o of toLocalize) {
pf1.config[o] = doLocalize(pf1.config[o], o);
}
for (const o of toLocalizeConst) {
pf1.const[o] = doLocalize(pf1.const[o], o);
}
// Localize buff targets
const localizeLabels = ["buffTargets", "buffTargetCategories", "contextNoteTargets", "contextNoteCategories"];
for (const l of localizeLabels) {
for (const [k, v] of Object.entries(pf1.config[l])) {
pf1.config[l][k].label = game.i18n.localize(v.label);
}
}
// Extra attack structure
doLocalizeKeys(pf1.config.extraAttacks, ["label", "flavor"]);
// Level-up data
doLocalizePaths(pf1.config.levelAbilityScoreFeature, ["name", "system.description.value"]);
// Point buy data
doLocalizeKeys(pf1.config.pointBuy, ["label"]);
});
/**
* This function runs after game data has been requested and loaded from the servers, so documents exist
*/
Hooks.once("setup", () => {
// Prepare registry data
for (const registry of Object.values(pf1.registry)) {
if (registry instanceof pf1.registry.Registry) registry.setup();
}
// Register controls
documents.controls.registerSystemControls();
Hooks.callAll("pf1PostSetup");
});
/* -------------------------------------------- */
/**
* Once the entire VTT framework is initialized, check to see if we should perform a data migration
*/
Hooks.once("ready", async function () {
// Create tooltip
const ttconf = game.settings.get("pf1", "tooltipConfig");
const ttwconf = game.settings.get("pf1", "tooltipWorldConfig");
if (!ttconf.disable && !ttwconf.disable) pf1.applications.TooltipPF.toggle(true);
window.addEventListener("resize", () => {
pf1.tooltip?.setPosition();
});
// Migrate data
const NEEDS_MIGRATION_VERSION = "10.5";
let PREVIOUS_MIGRATION_VERSION = game.settings.get("pf1", "systemMigrationVersion");
if (typeof PREVIOUS_MIGRATION_VERSION === "number") {
PREVIOUS_MIGRATION_VERSION = PREVIOUS_MIGRATION_VERSION.toString() + ".0";
} else if (
typeof PREVIOUS_MIGRATION_VERSION === "string" &&
PREVIOUS_MIGRATION_VERSION.match(/^([0-9]+)\.([0-9]+)$/)
) {
PREVIOUS_MIGRATION_VERSION = `${PREVIOUS_MIGRATION_VERSION}.0`;
}
const needMigration = SemanticVersion.fromString(NEEDS_MIGRATION_VERSION).isHigherThan(
SemanticVersion.fromString(PREVIOUS_MIGRATION_VERSION)
);
if (needMigration) {
const options = {};
// Omit dialog for new worlds with presumably nothing to migrate
if (PREVIOUS_MIGRATION_VERSION === "0.0.0") options.dialog = false;
await pf1.migrations.migrateWorld(options);
}
// Inform users who aren't running migration
if (!game.user.isGM && game.settings.get("pf1", "migrating")) {
ui.notifications.warn("PF1.Migration.InProgress", { localize: true });
}
// Migrate system settings
await documents.settings.migrateSystemSettings();
// Populate `pf1.applications.compendiums`
pf1.applications.compendiumBrowser.CompendiumBrowser.initializeBrowsers();
// Show changelog
if (!game.settings.get("pf1", "dontShowChangelog")) {
const v = game.settings.get("pf1", "changelogVersion");
const changelogVersion = SemanticVersion.fromString(v);
const curVersion = SemanticVersion.fromString(game.system.version);
if (curVersion.isHigherThan(changelogVersion)) {
const app = new pf1.applications.ChangeLogWindow(true);
app.render(true, { focus: true });
game.settings.set("pf1", "changelogVersion", curVersion.toString());
}
}
Hooks.callAll("pf1PostReady");
});
/* -------------------------------------------- */
/* Other Hooks */
/* -------------------------------------------- */
Hooks.on(
"renderChatMessage",
/**
* @param {ChatMessage} cm - Chat message instance
* @param {JQuery<HTMLElement>} jq - JQuery instance
* @param {object} options - Render options
*/
(cm, jq, options) => {
// Hide roll info
chatUtils.hideRollInfo(cm, jq, options);
// Hide GM sensitive info
chatUtils.hideGMSensitiveInfo(cm, jq, options);
// Hide non-visible targets for players
if (!game.user.isGM) chatUtils.hideInvisibleTargets(cm, jq[0]);
// Create target callbacks
chatUtils.addTargetCallbacks(cm, jq);
// Alter target defense options
chatUtils.alterTargetDefense(cm, jq);
// Optionally collapse the content
if (game.settings.get("pf1", "autoCollapseItemCards")) jq.find(".card-content").hide();
// Optionally hide chat buttons
if (game.settings.get("pf1", "hideChatButtons")) jq.find(".card-buttons").hide();
// Apply accessibility settings to chat message
chatUtils.applyAccessibilitySettings(cm, jq, options, game.settings.get("pf1", "accessibilityConfig"));
// Alter ammo recovery options
chatUtils.alterAmmoRecovery(cm, jq);
}
);
Hooks.on("renderChatPopout", (app, html, data) => {
// Optionally collapse the content
if (game.settings.get("pf1", "autoCollapseItemCards")) html.find(".card-content").hide();
// Optionally hide chat buttons
if (game.settings.get("pf1", "hideChatButtons")) html.find(".card-buttons").hide();
});
Hooks.on("renderChatLog", (_, html) => documents.item.ItemPF.chatListeners(html));
Hooks.on("renderChatLog", (_, html) => documents.actor.ActorPF.chatListeners(html));
Hooks.on("renderChatLog", (_, html) => _canvas.attackReach.addReachListeners(html));
Hooks.on("renderChatPopout", (_, html) => documents.item.ItemPF.chatListeners(html));
Hooks.on("renderChatPopout", (_, html) => documents.actor.ActorPF.chatListeners(html));
Hooks.on("renderAmbientLightConfig", (app, html) => {
_canvas.lowLightVision.addLowLightVisionToLightConfig(app, html);
});
Hooks.on("renderTokenHUD", (app, html, data) => {
_canvas.TokenQuickActions.addQuickActions(app, html, data);
});
// Hide token tooltip on token update or deletion
Hooks.on("deleteToken", (token) => pf1.tooltip?.unbind(token));
Hooks.on("updateToken", (token) => pf1.tooltip?.unbind(token));
Hooks.on("chatMessage", (log, message, chatData) => {
const result = documents.customRolls(message, chatData.speaker);
return !result;
});
Hooks.on("renderActorDirectory", (app, html, data) => {
html.find("li.actor").each((i, li) => {
li.addEventListener(
"drop",
applications.CurrencyTransfer._directoryDrop.bind(undefined, li.getAttribute("data-document-id"))
);
});
});
Hooks.on("renderItemDirectory", (app, html, data) => {
html.find("li.item").each((i, li) => {
li.addEventListener(
"drop",
applications.CurrencyTransfer._directoryDrop.bind(undefined, li.getAttribute("data-document-id"))
);
});
});
Hooks.on("dropActorSheetData", (act, sheet, data) => {
if (data.type === "Currency") sheet._onDropCurrency(event, data);
});
/* -------------------------------------------- */
/* Hotbar Macros */
/* -------------------------------------------- */
// Delay hotbarDrop handler registration to allow modules to override it.
Hooks.once("ready", () => {
Hooks.on("hotbarDrop", (bar, data, slot) => {
let macro;
const { type, uuid } = data;
switch (type) {
case "Item":
macro = macros.createItemMacro(uuid, slot);
break;
case "action":
macro = macros.createActionMacro(data.actionId, uuid, slot);
break;
case "skill":
macro = macros.createSkillMacro(data.skill, uuid, slot);
break;
case "save":
macro = macros.createSaveMacro(data.save, uuid, slot);
break;
case "defenses":
case "cmb":
case "concentration":
case "cl":
case "attack":
case "abilityScore":
case "initiative":
case "bab":
macro = macros.createMiscActorMacro(type, uuid, slot, data);
break;
default:
return true;
}
if (macro == null || macro instanceof Promise) return false;
});
});
// Render TokenConfig
Hooks.on(
"renderTokenConfig",
/**
* @param {TokenConfig} app - Config application
* @param {JQuery<HTMLElement>} html - HTML element
*/
async (app, html) => {
// Add vision inputs
let token = app.object;
// Prototype token
if (token instanceof Actor) token = token.prototypeToken;
const flags = token.flags?.pf1 ?? {};
// Add static size checkbox
const sizingTemplateData = { flags };
const sizeContent = await renderTemplate(
"systems/pf1/templates/foundry/token/token-sizing.hbs",
sizingTemplateData
);
const systemVision = game.settings.get("pf1", "systemVision");
html.find('.tab[data-tab="appearance"] > *:nth-child(3)').after(sizeContent);
const visionTab = html[0].querySelector(`.tab[data-tab="vision"]`);
// Disable vision elements if custom vision is disabled
const enableCustomVision = flags.customVisionRules === true || !systemVision;
let addDetectionModeButtonListener;
const toggleCustomVision = (enabled) => {
// Disable vision mode selection
visionTab.querySelector("select[name='sight.visionMode']").disabled = !enabled;
// Disable detection mode tab
const dmTab = visionTab.querySelector(".tab[data-tab='detection']");
for (const el of dmTab.querySelectorAll("input,select")) {
if (el.name === "flags.pf1.customVisionRules") continue;
el.disabled = !enabled;
}
// Disable detection mode tab buttons via CSS
dmTab.classList.toggle("disabled", !enabled);
};
if (!enableCustomVision) toggleCustomVision(enableCustomVision);
const visionContent = await renderTemplate("systems/pf1/templates/foundry/token/custom-vision.hbs", {
enabled: enableCustomVision || !systemVision,
noSystemVision: !systemVision,
});
$(visionTab).append(visionContent);
// Add listener for custom vision rules checkbox
// Soft toggle to work nicer with Foundry's preview behaviour
visionTab.querySelector(`input[name="flags.pf1.customVisionRules"]`).addEventListener("change", async (event) => {
toggleCustomVision(event.target.checked);
});
// Resize windows
app.setPosition();
}
);
// Render Sidebar
Hooks.on("renderSidebarTab", (app, html) => {
if (app instanceof Settings) {
// Add buttons
const chlogButton = $(`<button>${game.i18n.localize("PF1.Application.Changelog.Title")}</button>`);
const helpButton = $(`<button>${game.i18n.localize("PF1.Help.Label")}</button>`);
const tshooterButton = $(`<button>${game.i18n.localize("PF1.Troubleshooter.Button")}</button>`);
html
.find("#game-details")
.after(
$(`<h2>${game.i18n.localize("PF1.Title")}</h2>`),
$("<div id='pf1-details'>").append(chlogButton, helpButton, tshooterButton)
);
chlogButton.click(() => {
const chlog = Object.values(ui.windows).find((o) => o.id == "changelog") ?? new applications.ChangeLogWindow();
chlog.render(true, { focus: true });
});
helpButton.click(() => pf1.applications.helpBrowser.openUrl("Help/Home"));
tshooterButton.click(() => pf1.applications.Troubleshooter.open());
}
});
Hooks.on("controlToken", () => {
// Refresh lighting to (un)apply low-light vision parameters to them
canvas.perception.update(
{
initializeLighting: true,
},
true
);
});
/* ------------------------------- */
/* Expire active effects
/* ------------------------------- */
{
const expireFromTokens = function () {
if (game.users.activeGM?.isSelf) {
for (const t of canvas.tokens.placeables) {
// Skip tokens in combat to avoid too early expiration
if (t.combatant?.combat?.started) continue;
// Don't do anything for actors without this function (e.g. basic actors)
if (!t.actor?.expireActiveEffects) continue;
t.actor.expireActiveEffects();
}
}
};
// On game time change
Hooks.on("updateWorldTime", () => {
expireFromTokens();
});
// On canvas render
Hooks.on("canvasReady", () => {
expireFromTokens();
});
}
// Refresh skip state (alleviates sticky modifier issue #1572)
window.addEventListener("focus", () => (pf1.skipConfirmPrompt = false), { passive: true });