Initial commit: Fresh start with current state
This commit is contained in:
905
src/foundryvtt-pathfinder1-v10.8/pf1.mjs
Normal file
905
src/foundryvtt-pathfinder1-v10.8/pf1.mjs
Normal file
@@ -0,0 +1,905 @@
|
||||
/**
|
||||
* 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 });
|
||||
Reference in New Issue
Block a user