Skip to content
Idle edited this page Jan 17, 2025 · 24 revisions

Rule Element

The module uses its own Rule Element to allow the modification of cover and visibility on tokens, this make it possible to handle features, feats and other effects that would modify those.

Remember that adding anything to class, background, heritage and ancestry features/feats directly will not remain if they are reset in any way (e.g. lowering the character's level and back again), because of that, it is strongly advised to add the rule elements to world features/feats, either by making custom ones or by duplicating the system's.

Here follows a list of rule elements and macros that can be useful to your world and to illustrate how it all works.

Aim-Aiding Rune

add the following rule element

{"key":"PF2ePerception","type":"cover","selector":"ignored","targets":"allies"}

Alchemist Googles

add the following rule element to the item

{"key":"PF2ePerception","type":"cover","selector":"cancel","targets":"lesser","affects":"other","predicate":["item:group:bomb","item:trait:alchemical"]}

Blind Fight

Add the following rule elements to the feat

{"key":"PF2ePerception","type":"visibility","selector":"noff"}
{"key":"PF2ePerception","type":"visibility","affects":"other","selector":"dc","targets":"concealed","value":0}
{"key":"PF2ePerception","type":"visibility","affects":"other","selector":"dc","targets":"hidden","value":5}
{"key":"PF2ePerception","type":"visibility","affects":"other","selector":"reduce","targets":"undetected","predicate":[{"or":["target:distance:5","origin:distance:5"]},{"or":[{"gte":["self:level","target:level"]},{"gte":["self:level","origin:level"]}]}]}

Deny Advantage

Add the following rule element to the feat

{"key":"PF2ePerception","type":"visibility","selector":"noff","predicate":[{"gte":["self:level","origin:level"]}]}

Encroaching Presence

Add the following rule element to an effect you made for the Moderate Backlash

{"key":"PF2ePerception","type":"visibility","affects":"other","selector":"set","targets":"observed","value":"concealed"}

Faerie Fire

Create an effect and add the following rule elements to it

{"key":"PF2ePerception","type":"visibility","selector":"noinvis"}
{"key":"PF2ePerception","type":"visibility","selector":"cancel","targets":"concealed","predicate":[{"not":"self:condition:invisible"}]}

Faerie Fire (macro)

This macro will automatically find all the templates that were created by the spell Faerie Fire and apply the custom effect (created above) to the tokens inside the templates

const UUID = 'UUID_OF_THE_CUSTOM_FAERIE_FIRE_EFFECT'
const api = game.modules.get('pf2e-perception').api
const templates = canvas.scene.templates.filter(template => {
    const { type, slug } = template.getFlag('pf2e', 'origin') ?? {}
    return type === 'spell' && slug === 'faerie-fire'
})

const tokens = []
for (const template of templates) {
    const templateTokens = api.template.getTemplateTokens(template)
    tokens.push(...templateTokens.filter(t => t.actor))
}

const source = (await fromUuid(UUID)).toObject()
setProperty(source, 'flags.core.sourceId', UUID)

for (const token of tokens) {
    const actor = token.actor
    const exists = actor.itemTypes.effect.some(e => e.sourceId === UUID)
    if (!exists) await actor.createEmbeddedDocuments('Item', [source])
}

Methodical Debilitations

Create an effect for the second part of the feat and add the following rule elements to it

{"key":"PF2ePerception","type":"cover","selector":"ac","targets":["lesser","standard"],"value":0}
{"key":"PF2ePerception","type":"cover","selector":"ac","targets":["greater","greater-prone"],"value":2}

Mounted Combat (macro)

create a script macro with the following content

const uuid = 'Compendium.pf2e.other-effects.Item.9c93NfZpENofiGUp'
const mountee = canvas.tokens.controlled[0]?.actor
const mount = game.user.targets.first()

if (!mountee || !mount) {
    ui.notifications.error('You must select a token and target its mount.')
}

const exist = mountee.itemTypes.effect.find(i => i.sourceId === uuid)
if (exist) {
    exist.delete()
    return
}

const source = (await fromUuid(uuid)).toObject()
const rule = {
  "key": "PF2ePerception",
  "type": "cover",
  "selector": "ignore",
  "targets": mount.id
}

source.name += ` (${mount.name})`
source.system.rules.push(rule)
mountee.createEmbeddedDocuments('Item', [source])

To use it, select a token and target its mount then click on the macro, it will automatically add the Effect: Mounted to the selected token actor while adding the module's rule element which will ignore the mount from giving cover from and to the mounted actor.

API

A series of functions have been exposed to the global

/**
 * Retrieves the API object containing the functions
 */
game.modules.get('pf2e-perception').api
check: {
    /**
     * Retrieve the DC value if 'originToken' were to attack 'targetToken'
     * @param { Token | TokenDocument } originToken
     * @param { Token | TokenDocument } targetToken
     * @param { string[] } [extraOptions]
     * @returns { number } '0' means no check
     */
    getFlatCheckDc
}
geometry: {
    /**
     * clears all the debug lines present on the canvas
     */
    clearDebug,
    /**
     * @typedef { { A: { x: number; y: Number }; B: { x: number; y: number } } } Edge
     *
     * @param { { x: number; y: number; width: number; height: number; } } rect
     * @param { number } margin
     * @returns { { top: Edge; bottom: Edge; left: Edge; right: Edge } } the 4 edges coordinates after margin modification
     */
    getRectEdges,
    /**
     * @param { { x: number; y: number; } } origin
     * @param { { x: number; y: number; } } target
     * @param { boolean } [debug]
     * @returns { boolean } true if the line from `origin` to `target` intersects a wall that restricts movement
     */
    lineIntersectWall,
    /**
     * @param { { x: number; y: number; } } origin
     * @param { Token } token
     * @param { boolean } [debug]
     * @returns { boolean } true if any line from `origin` to a spread contained in the `token` bounds intersect a wall that restricts movement
     */
    pointToTokenIntersectWall,
}
token: {
    /**
     * @param { Token | TokenDocument } originToken
     * @param { Token | TokenDocument } targetToken
     * @param { perception: PerceptionRules = {}; debug: boolean = false } [options]
     * @returns { undefined | 'lesser' | 'standard' }
     */
    getCreatureCover,
    /**
     * @param { Token | TokenDocument } origin
     * @param { Token | TokenDocument } target
     * @param { boolean } [debug]
     * @returns { undefined | 'standard' }
     */
    getWallCover,
    /**
     * @param { Token | TokenDocument } origin
     * @param { Token | TokenDocument } target
     * @param { perception: PerceptionRules = {}; affects: 'origin' | 'target' = 'origin'; debug: boolean = false } [options]
     * @returns { undefined | 'concealed' | 'hidden' | 'undetected' | 'unnoticed' }
     */
    getVisibility,
    /**
     * removes the conditionals icons shown on a specific token or all tokens if not specified
     * @param { Token | TokenDocument } [token]
     */
    clearConditionals,
    /**
     * shows the conditionals icons that `origin` has in regard to `target`
     * @param { Token | TokenDocument } origin
     * @param { Token | TokenDocument } target
     */
    showConditionals,
    /**
     * shows the conditionals icons that all the tokens have in regard to `token`
     * @param { Token | TokenDocument } token
     */
    showAllConditionals,
    /**
     * @param { Token | TokenDocument } token
     * @param {...string} path
     * @returns { any } the data at current path from the `token`, the root data looks like:
     * Record<otherTokenId, { visibility?: 'concealed' | 'hidden' | 'undetected' | 'unnoticed'; cover: 'lesser' | 'standard' | 'greater' | 'greater-prone' }>
     */
    getTokenData,
    /**
     * origin, target, { perception = {}, options = [], affects = 'origin', debug = false } = {}
     * @param { Token | TokenDocument } origin
     * @param { Token | TokenDocument } target
     * @param { perception: PerceptionRules = {}; affects: 'origin' | 'target' = 'origin'; options: string[] = []; debug: boolean = false } [options]
     * @returns { undefined | 'lesser' | 'standard' | 'greater' | 'greater-prone' }
     */
    getCover,
    /**
     * opens the perception menu for `token`
     * @param { Token | TokenDocument } token
     */
    openHUD,
}
lighting: {
    /**
     * @param { Token | TokenDocument } token
     * @param { boolean } [debug]
     * @returns { undefined | null | 'dim' | 'bright' } returns undefined if the function didn't do any lighting check
     * (i.e. the token is invisible, the scene `Token Vision` is disabled or its level of darkness is below the threshold)
     */
    getLightExposure,
}
actor: {
    /**
     * @param { Actor } actor
     * @returns { boolean } if the actor is currently prone
     */
    isProne,
    /**
     * @param { Actor } actor
     * @param { boolean } [selection]
     * @returns { EffectPF2e | 'lesser' | 'standard' | 'greater' | 'greater-prone' | undefined } the cover effect currently on the actor or its selection value
     */
    getCoverEffect,
    /**
     * @param { Actor } actor
     * @returns { boolean } if the actor can see invisible tokens
     */
    seeInvisibility,
    /**
     * @param { Actor } actor
     * @returns { boolean } if the actor has greater darkvision
     */
    hasGreaterDarkvision,
}
scene: {
    /**
     * @param { Token | TokenDocument } token
     * @param { (Token | TokenDocument)[] } tokens
     * @returns { (Token | TokenDocument)[] } the tokens validated using `token` as a reference
     */
    validateTokens,
    /**
     * @param { Token | TokenDocument } token
     * @returns { TokenDocument[] } the list of valid token documents present on the same scene as `token`
     */
    getValidTokens,
    /**
     * @param { Scene } scene
     * @param { string } setting - a module setting that is global and scene based
     * @returns { any } the value of the setting from the `scene` or fallbacks to global
     */
    getSceneSetting,
}
template: {
    /**
     * will create a seek template (linked to `token`) that can be placed on the canvas
     * distance default to 30 for cones and 15 for the other types
     * @param { { token: Token | TokenDocument; type: 'burst' | 'emanation' | 'line' | 'cone' | 'rect' = 'burst'; distance?: number } } [options]
     */
    createSeekTemplate,
    /**
     * will create a darkness template that can be placed on the canvas
     * the `conceal` option will set tokens to concealed even from tokens with darkvision
     * @param { { type: 'burst' | 'emanation' | 'line' | 'cone' | 'rect' = 'burst'; distance: number = 20; conceal: boolean = false } } [options]
     */
    createDarknessTemplate,
    /**
     * will create a mist template that can be placed on the canvas
     * @param { { type: 'burst' | 'emanation' | 'line' | 'cone' | 'rect' = 'burst'; distance: number = 20 } } [options]
     */
    createMistTemplate,
    /**
     * @param { Token | TokenDocument } [token] - token reference to compare scenes
     * @returns { MeasuredTemplateDocument[] } the list of templates that have been tagged as `darkness` by the module
     */
    getDarknessTemplates,
    /**
     * @param { Token | TokenDocument } [token] - token reference to compare scenes
     * @returns { MeasuredTemplateDocument[] } the list of templates that have been tagged as `mist` by the module 
     */
    getMistTemplates,
    /**
     * @param { Token | TokenDocument } token - the token from which wall intersections should be checked
     * @returns { Token[] } the list of tokens contained in all the templates that have been tagged as `seek` by the module
     */
    getSeekTemplateTokens,
    /**
     * deletes all templates that have been tagged as `seek` by the module and linked to `token`
     * @param { Token | TokenDocument } token
     */
    deleteSeekTemplate,
    /**
     * template, { collisionOrigin, collisionType = 'move' } = {}
     * @param { MeasuredTemplateDocument } template
     * @param { { collisionOrigin: { x: number; y: number } = template.center, collisionType = 'move' } } [options]
     * @returns { Token[] } the list of tokens contained in the template, each token will also be checked for collision
     */
    getTemplateTokens,
}
ruleElement: {
    /**
     * @param { Token | TokenDocument } origin
     * @param { Token | TokenDocument } target
     * @param { { distance?: number; extraOptions = [] } } [options]
     * @returns { PerceptionRules } formated object of rules related to `origin` and `target`
     */
    perceptionRules,
    /**
     * @param { PerceptionRules } perception
     * @param { 'origin' | 'target' } affects
     * @param { 'visibility' | 'cover' } type
     * @param { string } selector - selector based on the rule element `type`
     * @param { string | string[] } [targets] - selector targets
     * @returns { any } the existing value
     */
    getPerception,
    /**
     * @param { PerceptionRules } perception
     * @param { 'origin' | 'target' } affects
     * @param { 'visibility' | 'cover' } type
     * @param { any } value - the current value
     * @returns { any } the updated value using the perception rule elements
     */
    updateFromPerceptionRules,
}