diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b9dd06..8b5061a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: "18" + node-version: "20" cache: "yarn" - run: | diff --git a/.gitignore b/.gitignore index 331193c..9203d64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist/ node_modules/ .vscode/ +.vite-cache/ diff --git a/module.json b/module.json index 79b3560..decc182 100644 --- a/module.json +++ b/module.json @@ -52,7 +52,7 @@ "dist/module.js" ], "styles": [ - "styles/main.css" + "dist/style.css" ], "flags": { "hotReload": { diff --git a/package.json b/package.json index e6b5f2d..66e8c2f 100644 --- a/package.json +++ b/package.json @@ -4,19 +4,21 @@ "author": "oWave", "license": "MIT", "private": true, + "type": "module", "scripts": { - "watch": "esbuild src/index.ts --outfile=dist/module.js --sourcemap --bundle --watch", - "build": "esbuild src/index.ts --outfile=dist/module.js --bundle", - "lint": "eslint src/", + "dev": "vite serve", + "build": "vite build", "unpack": "fvtt package workon \"pf2e-flatcheck-helper\" && fvtt package unpack \"effects\"", "pack": "fvtt package workon \"pf2e-flatcheck-helper\" && fvtt package pack \"effects\"" }, "devDependencies": { "@biomejs/biome": "^1.8.3", "@types/jquery": "^3.5.30", + "@types/node": "^22.3.0", "typescript": "*", "vite": "^5.4.0", - "vite-plugin-checker": "^0.7.2" + "vite-plugin-checker": "^0.7.2", + "vite-tsconfig-paths": "^5.0.1" }, "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447", "dependencies": {} diff --git a/src/flat.ts b/src/flat.ts deleted file mode 100644 index b5919fa..0000000 --- a/src/flat.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { TokenPF2e } from "types/pf2e/module/canvas" -import Module from "./index" -import module from "./index" -import { MoreDialog } from "./more-dialog" - -export async function rollFlatCheck( - dc: number, - { hidden = false, label }: { hidden: boolean; label?: string }, -) { - const r = await new Roll("d20").roll() - const degree = r.total >= dc ? "success" : "failure" - const delta = r.total - dc - const deltaText = delta > 0 ? `+${delta}` : delta.toString() - - const flavor = document.createElement("span") - flavor.classList.add("flavor-text") - flavor.innerHTML = ` -
-
${label ?? "Flat"} DC: ${dc}
-
- Result: ${degree.capitalize()} - by ${deltaText} -
-
- ` - - r.toMessage({ flavor: flavor.outerHTML }, { rollMode: hidden ? "blindroll" : "roll" }) -} - -export const CONDITION_DCS = { - concealed: 5, - hidden: 11, - invisible: 11, -} - -function dcForToken(token: TokenPF2e) { - let dc = 0 - token.actor?.conditions.stored.forEach((c) => { - dc = Math.max(CONDITION_DCS[c.slug] ?? 0, dc) - }) - return dc || null -} - -export async function rollForSingleTarget( - target: TokenPF2e | undefined, - { hidden = false }: { hidden: boolean }, -) { - if (!target) return - const dc = dcForToken(target) - if (!dc) ui.notifications.warn("Selected target has no conditions that require a flat check") - else rollFlatCheck(dc, { hidden }) -} - -export function setupFlat() { - if (!module.settings.fcButtonsEnabled) return - - Hooks.on("renderSidebarTab", async (app: SidebarTab, html: HTMLCollection) => { - if (app.tabName !== "chat") return - - const chat = html[0].querySelector("#chat-form") - - const template = await renderTemplate("modules/pf2e-flatcheck-helper/templates/buttons.hbs", {}) - const node = document.createElement("div") - node.id = "fc-container" - // node.style.flex = "0" - node.innerHTML = template - - chat?.after(node) - const $node = $(node) - // @ts-expect-error - $node.find(".tooltip").tooltipster({ - contentAsHTML: true, - }) - - node.querySelectorAll("button").forEach((button) => - button.addEventListener("click", function (e) { - const value = this.dataset.dc - if (!value) throw new Error(`Bad button DC value ${value}`) - const hidden = e.ctrlKey - - if (value === "targets") { - if (game.user?.targets.size === 0) return ui.notifications.warn("No targets selected") - if (game.user?.targets.size === 1) - return rollForSingleTarget(game.user.targets.first(), { hidden }) - else return ui.notifications.warn("Too many targets") - } else if (value === "more") { - new MoreDialog().render(true) - } else { - const dc = Number(value) - if (Number.isNaN(dc)) throw new Error(`Bad button DC value ${value}`) - - rollFlatCheck(dc, { hidden }) - } - }), - ) - }) - - Hooks.on("targetToken", (user) => { - if (user.id !== game.user?.id) return - if (game.user.targets.size !== 1) - return document.querySelector("#fc-button-target")?.classList.remove("highlight") - const effectSlugs = Object.keys(CONDITION_DCS) - - if (game.user?.targets?.first()?.actor?.conditions.some((c) => effectSlugs.includes(c.slug))) - document.querySelector("#fc-button-target")?.classList.add("highlight") - else document.querySelector("#fc-button-target")?.classList.remove("highlight") - }) -} diff --git a/src/index.ts b/src/index.ts index e8051f9..9837c6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ import { MODULE_ID } from "./constants" -import { setupDelay } from "./delay" -import { setupEmanationAutomation } from "./emanation" -import { setupFlat } from "./flat" -import { setupLink } from "./life-link" +import { DelayModule } from "./modules/delay" +import { EmanationModule } from "./modules/emanation/emanation" import { settings } from "./settings" +import { FlatModule } from "./modules/flat/flat" +import { LifeLinkModule } from "./modules/life-link" type Callback = (data: any) => void @@ -21,6 +21,9 @@ class SocketHandler { register(type: string, handler: Callback) { this.#callbacks[type] = handler } + unregister(type: string) { + delete this.#callbacks[type] + } emit(type: string, data: any) { if (!(type in this.#callbacks)) { @@ -33,24 +36,51 @@ class SocketHandler { } } -const module = { +const MODULE = { socketHandler: new SocketHandler(), settings, + modules: { + flat: new FlatModule(), + delay: new DelayModule(), + emanation: new EmanationModule(), + lifeLink: new LifeLinkModule(), + }, } -export default module +export default MODULE Hooks.on("init", () => { - module.settings.init() - module.socketHandler.init() + MODULE.settings.init() + MODULE.socketHandler.init() + + for (const [name, module] of Object.entries(MODULE.modules)) { + const enabled = + module.settingsKey == null + ? true + : (game.settings.get(MODULE_ID, module.settingsKey) as boolean) - setupDelay() - setupFlat() - setupLink() + if (enabled) module.enable() + } }) Hooks.on("ready", () => { - setupEmanationAutomation() + for (const [name, module] of Object.entries(MODULE.modules)) { + if (module.enabled) module.onReady() + } +}) + +Hooks.on("updateSetting", (setting: { key: string }, data) => { + if (!setting.key.startsWith(MODULE_ID)) return + + const key = setting.key.split(".", 2).at(1) + if (!key) return + + for (const m of Object.values(MODULE.modules).filter((m) => m.settingsKey === key)) { + if (data.value === "true") { + m.enable() + if (m.enabled) m.onReady() + } else if (data.value === "false") m.disable() + } }) Hooks.on("renderSettingsConfig", (app: SettingsConfig, $html: JQuery) => { diff --git a/src/life-link.ts b/src/life-link.ts deleted file mode 100644 index 8be2462..0000000 --- a/src/life-link.ts +++ /dev/null @@ -1,318 +0,0 @@ -import type { ActorPF2e } from "types/pf2e/module/actor" -import type { ChatMessagePF2e } from "types/pf2e/module/chat-message" -import type { CombatantPF2e, EncounterPF2e } from "types/pf2e/module/encounter" -import type { EffectPF2e, ItemPF2e } from "types/pf2e/module/item" -import Module from "./index" -import module from "./index" -import { actorEffectBySlug, actorHasEffect } from "./utils" - -interface ButtonArgs { - // HP to transfer from source to target - // Stops if source doesn't have enough HP - transfer?: number - // HP to restore to target - heal?: number - // HP to remove from source - dmg?: number - source: string - target: string - cd?: 1 -} - -const UNDO_BUTTON_MARKUP = `` - -function makeButton(label: string, args: ButtonArgs) { - const json = JSON.stringify(args) - return ` - - - ${label} - - ` -} - -async function updateHP(actor, delta) { - const hp = actor.system.attributes.hp.value - await actor.update({ - "system.attributes.hp.value": hp + delta, - }) -} - -async function handleTransferButton(args: ButtonArgs) { - const required = ["source", "target"] - for (const k of required) { - // biome-ignore lint/style/useTemplate: - if (!(k in args)) return ui.notifications.error("Missing arg " + k) - } - - const source = fromUuidSync(args.source) - const target = fromUuidSync(args.target) - if (!target) return ui.notifications.error("No target actor") - if (!source) return ui.notifications.error("No source actor") - if (source.id === target.id) return ui.notifications.error("Can't transfer damage to self!") - - let transfer = 0 - - if (args.transfer) { - const missingHP = - // @ts-expect-error pf2e - target.system.attributes.hp.max - target.system.attributes.hp.value - const maxTransfer = Math.min(Number(args.transfer), missingHP) - - // @ts-expect-error pf2e - const hpRemaining = source.system.attributes.hp.value - transfer = Math.min(maxTransfer, hpRemaining) - - if (transfer <= 0) return ui.notifications.warn("No HP remaining to transfer.") - } - - let heal = transfer - let dmg = transfer - - // If the HP transfer (from life link) reduces the source to 0, share life no longer applies - // @ts-expect-error pf2e - if (transfer < source.system.attributes.hp.value) { - heal += args.heal ?? 0 - dmg += args.dmg ?? 0 - } - await updateHP(source, -dmg) - await updateHP(target, heal) - - if (args.cd) { - await target.createEmbeddedDocuments("Item", [ - { - type: "effect", - name: "Life Link CD", - img: "systems/pf2e/icons/spells/life-link.webp", - system: { - tokenIcon: { show: true }, - duration: { - value: 1, - unit: "rounds", - sustained: false, - expiry: "turn-start", - }, - slug: "life-link-cd", - }, - }, - ]) - } - - // @ts-expect-error Using uuids as keys doesn't work, but this does. Only question is when does this break - await ChatMessage.create({ - content: ` - ${target.name} +${heal} HP - 🡰 - ${source.name} -${dmg} HP - - ${UNDO_BUTTON_MARKUP} - `, - flags: { - undo: [ - [source.uuid, dmg], - [target.uuid, -heal], - ], - }, - }) -} - -function handleSpiritLink(effect: EffectPF2e) { - const { actor, origin } = effect - - if (!actor) { - return null - } - - if (!origin || origin.id === actor.id) { - ui.notifications.error( - `Bad origin actor for Spirit Linked effect on ${actor.name}! See module readme.`, - ) - return null - } - - const transfer = effect.level * 2 - const missingHP = actor.system.attributes.hp!.max - actor.system.attributes.hp!.value - if (missingHP <= 0) return null - - return makeButton(`${transfer} HP to ${actor.name}`, { - transfer, - source: origin.uuid, - target: actor.uuid, - }) -} - -export function setupLink() { - Hooks.on<[CombatantPF2e, EncounterPF2e]>("pf2e.startTurn", async (combatant) => { - if (!module.settings.lifeLinkEnabled) return - if (game?.users?.activeGM?.id !== game.user?.id) return - - const links: string[] = [] - - canvas.scene?.tokens.forEach(({ actor }) => { - if (!actor) return - const e = actorEffectBySlug(actor, "spirit-linked") - if (!e) return - if (combatant.actor?.id !== e.origin?.id) return - - const link = handleSpiritLink(e) - if (link) links.push(link) - }) - - const content = `Spirit Link
${links.join("
")}` - - if (links.length) { - await ChatMessage.create({ - content: content, - whisper: ChatMessage.getWhisperRecipients("GM").map((u) => u.id), - speaker: ChatMessage.getSpeaker({ actor: combatant.actor }), - }) - } - }) - - Hooks.on<[ItemPF2e]>("createItem", async (item) => { - if (item.isOfType("effect") && item.slug === "spirit-linked") { - const link = handleSpiritLink(item) - if (link) { - await ChatMessage.create({ - content: `Spirit Link
${link}`, - whisper: ChatMessage.getWhisperRecipients("GM").map((u) => u.id), - speaker: ChatMessage.getSpeaker({ actor: item.actor }), - }) - } - } - }) - - Hooks.on("createChatMessage", async (msg) => { - if (!module.settings.lifeLinkEnabled) return - if (game.users?.activeGM?.id !== game.user?.id) return - - const flags = (msg).flags?.pf2e?.appliedDamage - const uuid = flags?.uuid - const dmg = flags?.updates.find((e) => e.path === "system.attributes.hp.value")?.value - if (!uuid || !dmg || dmg <= 0) return - - const actor = fromUuidSync(uuid) as ActorPF2e - if (!actor) return - - let lifeLinkTransfer = 0 - - const lifeLinkEffect = actorEffectBySlug(actor, "life-linked") - if (lifeLinkEffect && !actorHasEffect(actor, "life-link-cd")) { - lifeLinkTransfer = (() => { - if (!lifeLinkEffect.origin || lifeLinkEffect.origin.id === actor.id) { - ui.notifications.error( - `Bad origin actor for Life Linked effect on ${actor.name}! See module readme.`, - { - permanent: true, - }, - ) - return 0 - } - - let maxTransfer = 3 - if (module.settings.lifeLinkVariant === "plus") - maxTransfer = 2 + Math.floor((lifeLinkEffect.level - 1) / 2) * 3 - else { - if (lifeLinkEffect.level >= 3) maxTransfer = 5 - if (lifeLinkEffect.level >= 6) maxTransfer = 10 - if (lifeLinkEffect.level >= 9) maxTransfer = 15 - } - - return Math.min(maxTransfer, dmg) - })() - } - - const shareLifeEffect = actorEffectBySlug(actor, "share-life") - - if (shareLifeEffect && !shareLifeEffect?.origin) - ui.notifications.error( - `Bad origin actor for Share Life effect on ${actor.name}! See module readme.`, - { - permanent: true, - }, - ) - - const buttons: string[] = [] - ;(() => { - if (shareLifeEffect && lifeLinkTransfer) { - const remainingDmg = dmg - lifeLinkTransfer - - if ( - shareLifeEffect?.origin && - lifeLinkEffect?.origin && - shareLifeEffect.origin.uuid === lifeLinkEffect.origin.uuid - ) { - // Both effects from the same source -> One Button - buttons.push( - makeButton( - `${Math.ceil(remainingDmg / 2) + lifeLinkTransfer} to ${lifeLinkEffect.origin.name}`, - { - transfer: lifeLinkTransfer, - heal: remainingDmg === 1 ? 1 : Math.floor(remainingDmg / 2), - dmg: Math.ceil(remainingDmg / 2), - cd: 1, - source: lifeLinkEffect.origin.uuid, - target: actor.uuid, - }, - ), - ) - return - } - } - // return above means this is unreachable if both effects are from the same source - if (shareLifeEffect?.origin) { - const remainingDmg = dmg - lifeLinkTransfer - // Button for Share Life - if (remainingDmg) - buttons.push( - makeButton( - `(Share Life) ${Math.ceil(remainingDmg / 2)} to ${shareLifeEffect.origin.name}`, - { - heal: remainingDmg === 1 ? 1 : Math.floor(remainingDmg / 2), - dmg: Math.ceil(remainingDmg / 2), - source: shareLifeEffect.origin.uuid, - target: actor.uuid, - }, - ), - ) - } - if (lifeLinkEffect?.origin && lifeLinkTransfer) { - buttons.push( - makeButton(`(Life Link) ${lifeLinkTransfer} to ${lifeLinkEffect.origin.name}`, { - transfer: lifeLinkTransfer, - cd: 1, - source: lifeLinkEffect.origin.uuid, - target: actor.uuid, - }), - ) - } - })() - - if (buttons.length) { - await ChatMessage.create({ - content: `Damage Transfer
${buttons.join("
")}`, - whisper: ChatMessage.getWhisperRecipients("GM").map((u) => u.id), - speaker: ChatMessage.getSpeaker(actor), - }) - } - }) - - Hooks.on("renderChatMessage", (msg, html) => { - if (!game.user?.isGM) return - html.find("a.life-link").on("click", async (event) => { - const args = JSON.parse(event.target.dataset.args!) as ButtonArgs - await handleTransferButton(args) - }) - html.find("button.fc-undo-button").on("click", async () => { - const data = msg.flags.undo as unknown as [string, number][] - for (const [uuid, dmg] of data) { - const actor = await fromUuid(uuid) - await updateHP(actor, dmg) - } - html.find(".undo-text").addClass("undo") - await msg.update({ - content: html.find(".message-content").html(), - }) - }) - }) -} diff --git a/styles/main.css b/src/main.css similarity index 83% rename from styles/main.css rename to src/main.css index 6316b78..0e6b07d 100644 --- a/styles/main.css +++ b/src/main.css @@ -79,3 +79,18 @@ border: unset; } } + +#pf2e-hud-tracker .combatant .initiative { + .delay-indicator { + animation: rotating 4s linear infinite; + } + + .delay-return .delay-indicator:hover { + animation: unset; + + &:before { + /* fa-play */ + content: "\f04b"; + } + } +} diff --git a/src/modules/base.ts b/src/modules/base.ts new file mode 100644 index 0000000..79410cf --- /dev/null +++ b/src/modules/base.ts @@ -0,0 +1,63 @@ +import MODULE from "src/index" +import { MODULE_ID } from "src/constants" + +export abstract class BaseModule { + enabled = false + hooks: Record = {} + wrappers: number[] = [] + sockets: string[] = [] + settingListeners: string[] = [] + + abstract readonly settingsKey: string | null + + onReady() {} + + enable() { + this.enabled = true + } + disable() { + this.enabled = false + + for (const [hook, id] of Object.entries(this.hooks)) { + // @ts-expect-error + Hooks.off(hook, id) + } + this.hooks = {} + + for (const id of this.wrappers) { + libWrapper.unregister(MODULE_ID, id) + } + this.wrappers = [] + + for (const type of this.sockets) { + MODULE.socketHandler.unregister(type) + } + this.sockets = [] + + for (const key of this.settingListeners) { + MODULE.settings.removeListener(key) + } + this.settingListeners = [] + } + + registerHook(hook: string, callback: HookCallback) { + this.hooks[hook] = Hooks.on(hook, callback) + } + + registerWrapper( + target: string, + callback: CallableFunction, + mode: "MIXED" | "WRAPPER" | "OVERRIDE", + ) { + this.wrappers.push(libWrapper.register(MODULE_ID, target, callback, mode)) + } + + registerSocket(type: string, callback: (data: any) => void) { + this.sockets.push(type) + MODULE.socketHandler.register(type, callback) + } + + registerSettingListener(key: string, callback: (value: unknown) => void) { + MODULE.settings.addListener(key, callback) + } +} diff --git a/src/delay.ts b/src/modules/delay/index.ts similarity index 51% rename from src/delay.ts rename to src/modules/delay/index.ts index 71fc95b..5ce4981 100644 --- a/src/delay.ts +++ b/src/modules/delay/index.ts @@ -1,59 +1,128 @@ -import type { ActorPF2e } from "types/pf2e/module/actor" +import type { CombatantPF2e, EncounterPF2e, RolledCombatant } from "types/pf2e/module/encounter" import type { ChatMessagePF2e } from "types/pf2e/module/chat-message" -import type { CombatantPF2e, EncounterPF2e } from "types/pf2e/module/encounter" +import MODULE from "src/index" +import { BaseModule } from "../base" +import { combatantIsNext, isJQuery, sleep } from "src/utils" import type { TokenDocumentPF2e } from "types/pf2e/module/scene" -import { MODULE_ID } from "./constants" -import module from "./index" -import { combatantIsNext, isJQuery, sleep } from "./utils" +import { MODULE_ID } from "src/constants" +import type { ActorPF2e } from "types/pf2e/module/actor" +import { onRenderCombatTracker } from "./tracker" +import { isDelaying, setInitiativeFromDrop } from "./utils" +import { onRenderPF2eHudTracker } from "./pf2e-hud" -async function applyDelayEffect(actor: ActorPF2e) { - return actor.createEmbeddedDocuments("Item", [ - { - type: "effect", - name: "Delay", - img: "icons/svg/clockwork.svg", - system: { - tokenIcon: { show: true }, - duration: { - value: -1, - unit: "encounter", - sustained: false, - expiry: "turn-start", - }, - slug: "x-delay", - }, - }, - ]) -} +export class DelayModule extends BaseModule { + settingsKey = null -function isDelaying(actor: ActorPF2e) { - return actor.items.some((e) => e.slug === "x-delay") -} + enable() { + super.enable() -function removeDelaying(actor: ActorPF2e) { - const e = actor.items.find((e) => e.slug === "x-delay") - if (e?.id) return actor.deleteEmbeddedDocuments("Item", [e.id]) + this.registerHook("renderEncounterTrackerPF2e", onRenderCombatTracker) + this.registerHook("renderTokenHUD", onRenderTokenHUD) + this.registerHook("renderPF2eHudTracker", onRenderPF2eHudTracker) + this.registerHook("updateCombat", onUpdateCombat) + this.registerHook("createChatMessage", onCreateMessage) + + this.registerSocket("moveAfter", socketMoveAfter) + } } +function onRenderTokenHUD(app: TokenHUD, html: JQuery) { + if (MODULE.settings.showInTokenHUD) { + const token = app.object + const combatant = token?.combatant as CombatantPF2e + if ( + combatant?.parent?.started && + !!combatant && + combatant.initiative != null && + combatant.actor && + combatant.isOwner + ) { + const column = html.find("div.col.right") -const sortedCombatants = () => { - if (!game.combat) throw new Error("No combat?") - return game - .combat!.combatants.filter((e) => e.initiative !== null) - .sort((a, b) => { - const resolveTie = (): number => { - const [priorityA, priorityB] = [a, b].map( - (c): number => - c.overridePriority(c.initiative ?? 0) ?? c.actor?.initiative?.tiebreakPriority ?? 3, - ) - return priorityA === priorityB ? a.id.localeCompare(b.id) : priorityA - priorityB + if (isDelaying(combatant.actor)) { + if (!combatantIsNext(combatant) && MODULE.settings.allowReturn) { + $(` +
+ +
`) + .on("click", (e) => { + if (combatant.actor && isDelaying(combatant.actor) && !combatantIsNext(combatant)) { + tryReturn(combatant) + e.currentTarget.style.display = "none" + } + }) + .appendTo(column) + } + } else if (combatant.parent.combatant?.id === combatant.id) { + $(` +
+ +
`) + .on("click", (e) => { + if (combatant.parent?.combatant?.id === combatant.id) tryDelay() + }) + .appendTo(column) } + } + } - return typeof a.initiative === "number" && - typeof b.initiative === "number" && - a.initiative === b.initiative - ? resolveTie() - : b.initiative! - a.initiative! || (a.id > b.id ? 1 : -1) - }) + if (MODULE.settings.removeCombatToggle) { + const token = app.object + const combatant = token?.combatant + if (combatant?.parent.started && !!combatant && combatant.initiative != null) { + const toggleCombatButton = html.find("div.control-icon[data-action=combat]") + toggleCombatButton?.hide() + } + } +} +function onUpdateCombat(combat: EncounterPF2e) { + if (game.user && game.user.id !== game.users?.activeGM?.id) return + if (!combat.combatant?.actor) return + removeDelayEffect(combat.combatant.actor) +} +function onCreateMessage(msg: ChatMessagePF2e) { + if (msg?.author?.id !== game.user?.id) return + if (!game.combat?.started) return + const item = msg?.item + if (item?.actor?.isOwner && item?.type === "action" && item.slug === "delay") + if (isDelaying(item.actor) && item.actor.combatant) { + if (MODULE.settings.allowReturn) tryReturn(item.actor.combatant, { skipMessage: true }) + } else if (item.actor.id === game.combat.combatant?.actorId) tryDelay({ skipMessage: true }) +} + +function socketMoveAfter({ + combatId, + combatantId, + afterId, +}: { + combatId: string + combatantId: string + afterId: string +}) { + if (game.users.activeGM?.id !== game.user.id) return + + const combat = game.combats?.get(combatId) as EncounterPF2e + if (!combat || combat.id !== ui.combat.viewed?.id) return + const combatant = combat.combatants.get(combatantId) as RolledCombatant + const after = combat.combatants.get(afterId) + + if (!combatant || !after || combatant.initiative == null || after.initiative == null) return + + const newOrder = combat.turns.filter( + (c): c is RolledCombatant => + typeof c.initiative === "number" && c.id !== combatant.id, + ) + const afterIndex = newOrder.findIndex((c) => c.id === afterId) + newOrder.splice(afterIndex + 1, 0, combatant) + + setInitiativeFromDrop(combat, newOrder, combatant) + + combat.setMultipleInitiatives( + newOrder.map((c) => ({ + id: c.id, + value: c.initiative, + overridePriority: c.overridePriority(c.initiative), + })), + ) } interface TryDelayOptions { @@ -67,7 +136,7 @@ export function tryDelay(opts?: TryDelayOptions) { if (!c) return ui.notifications.error("No combatant") if (!c.token?.isOwner) return ui.notifications.error("You do not own the current combatant") - const combatants = sortedCombatants() + const combatants = combat.turns const currentId = combatants.findIndex((e) => e.id === c.id) const options = combatants @@ -82,7 +151,7 @@ export function tryDelay(opts?: TryDelayOptions) { return `` }) - if (!module.settings.delayShouldPrompt) { + if (!MODULE.settings.delayShouldPrompt) { if (!opts?.skipMessage) createMessage(c.token, "Delay") if (c.actor) applyDelayEffect(c.actor) combat.nextTurn() @@ -134,110 +203,40 @@ export function tryDelay(opts?: TryDelayOptions) { ).render(true) } -function tryReturn(combatant: CombatantPF2e, opts?: TryDelayOptions) { +export function tryReturn(combatant: CombatantPF2e, opts?: TryDelayOptions) { if (game.combat?.combatant && !combatantIsNext(combatant)) { if (!opts?.skipMessage && combatant.token) createMessage(combatant.token, "Return") emitMoveAfter(game.combat.id, combatant.id, game.combat.combatant.id) } } -interface MoveAfterPayload { - combatId: string - combatantId: string - afterId: string -} - -function emitMoveAfter(combatId: string, combatantId: string, afterId: string) { - module.socketHandler.emit("moveAfter", { combatId, combatantId, afterId }) +async function applyDelayEffect(actor: ActorPF2e) { + return actor.createEmbeddedDocuments("Item", [ + { + type: "effect", + name: "Delay", + img: "icons/svg/clockwork.svg", + system: { + tokenIcon: { show: true }, + duration: { + value: -1, + unit: "encounter", + sustained: false, + expiry: "turn-start", + }, + slug: "x-delay", + }, + }, + ]) } -export function moveAfter({ combatId, combatantId, afterId }: MoveAfterPayload) { - if (game.users.activeGM?.id !== game.user.id) return - - const combat = game.combats?.get(combatId) as EncounterPF2e - if (!combat) return - const combatant = combat.combatants.get(combatantId) - const after = combat.combatants.get(afterId) - - if (!combatant || !after) return - - const targetInitiative = after.initiative - if (!targetInitiative) return - - const order = combat.turns - .filter((c) => c.id !== combatantId) - .map((c) => { - return { - id: c.id, - initiative: c.initiative, - } - }) - let afterIndex = combat.turns.findIndex((c) => c.id === afterId) - if (afterIndex === 0) afterIndex++ - - const newOrder = [ - ...order.slice(0, afterIndex), - { id: combatant.id, initiative: targetInitiative }, - ...order.slice(afterIndex), - ] - - const updates: { - id: string - value: number - overridePriority: number - }[] = [] - - const withSameInitiative = newOrder.filter((c) => c.initiative === targetInitiative) - for (const [i, { id }] of withSameInitiative.entries()) { - updates.push({ - id: id!, - value: targetInitiative!, - overridePriority: i, - }) - } - - /* - console.log("--Updates--") - updates.forEach((e) => { - const c = game.combat?.combatants.get(e.id) - console.log( - `${c?.name} ${c?.initiative} ${c?.flags.pf2e.overridePriority[c?.initiative] ?? "-"} -> ${e.value} ${ - e.overridePriority - }` - ) - }) - */ - game.combat?.setMultipleInitiatives(updates).catch((e) => { - throw e - }) +function removeDelayEffect(actor: ActorPF2e) { + const e = actor.items.find((e) => e.slug === "x-delay") + if (e?.id) return actor.deleteEmbeddedDocuments("Item", [e.id]) } -function drawButton(type: "delay" | "return", combatentHtml: JQuery, combatant: CombatantPF2e) { - let button = $(` -
- -
- `) - if (type === "return") { - const title = module.settings.allowReturn ? "Return to initiative" : "Delaying" - const cls = module.settings.allowReturn ? "initiative-return" : "initiative-delay-indicator" - button = $(` -
- - -
- `) - } - - const div = combatentHtml.find(".token-initiative") - div.find(".initiative").hide() - div.append(button) - - button.on("click", (e) => { - e.stopPropagation() - if (type === "delay") tryDelay() - else if (module.settings.allowReturn) tryReturn(combatant) - }) +function emitMoveAfter(combatId: string, combatantId: string, afterId: string) { + MODULE.socketHandler.emit("moveAfter", { combatId, combatantId, afterId }) } function createMessage(token: TokenDocumentPF2e, title: string) { @@ -251,92 +250,3 @@ function createMessage(token: TokenDocumentPF2e, title: string) { `, }) } - -function onRenderCombatTracker(tracker, html: JQuery, data) { - if (!module.settings.showInCombatTracker) return - const combat = game.combat - if (!combat || !combat.started) return - - html.find(".combatant.actor").each((i, e) => { - const id = e.dataset.combatantId - if (!id) return - const c = combat.combatants.get(id) - if (!c || !c.isOwner || c.initiative == null) return - - if (combat.combatant?.id === c.id) drawButton("delay", $(e), c) - else if (c.actor && isDelaying(c.actor)) drawButton("return", $(e), c) - }) -} - -function onRenderTokenHUD(app: TokenHUD, html: JQuery) { - if (module.settings.showInTokenHUD) { - const token = app.object - const combatant = token?.combatant as CombatantPF2e - if ( - combatant?.parent?.started && - !!combatant && - combatant.initiative != null && - combatant.actor && - combatant.isOwner - ) { - const column = html.find("div.col.right") - - if (isDelaying(combatant.actor)) { - if (!combatantIsNext(combatant) && module.settings.allowReturn) { - $(` -
- -
`) - .on("click", (e) => { - if (combatant.actor && isDelaying(combatant.actor) && !combatantIsNext(combatant)) { - tryReturn(combatant) - e.currentTarget.style.display = "none" - } - }) - .appendTo(column) - } - } else if (combatant.parent.combatant?.id === combatant.id) { - $(` -
- -
`) - .on("click", (e) => { - if (combatant.parent?.combatant?.id === combatant.id) tryDelay() - }) - .appendTo(column) - } - } - } - - if (module.settings.removeCombatToggle) { - const token = app.object - const combatant = token?.combatant - if (combatant?.parent.started && !!combatant && combatant.initiative != null) { - const toggleCombatButton = html.find("div.control-icon[data-action=combat]") - toggleCombatButton?.hide() - } - } -} - -export function setupDelay() { - Hooks.on("renderEncounterTrackerPF2e", onRenderCombatTracker) - Hooks.on("renderTokenHUD", onRenderTokenHUD) - - Hooks.on<[EncounterPF2e]>("updateCombat", (combat) => { - if (game.user && game.user.id !== game.users?.activeGM?.id) return - if (!combat.combatant?.actor) return - removeDelaying(combat.combatant.actor) - }) - - Hooks.on<[ChatMessagePF2e]>("createChatMessage", (msg) => { - if (msg?.author?.id !== game.user?.id) return - if (!game.combat?.started) return - const item = msg?.item - if (item?.actor?.isOwner && item?.type === "action" && item.slug === "delay") - if (isDelaying(item.actor) && item.actor.combatant) { - if (module.settings.allowReturn) tryReturn(item.actor.combatant, { skipMessage: true }) - } else if (item.actor.id === game.combat.combatant?.actorId) tryDelay({ skipMessage: true }) - }) - - module.socketHandler.register("moveAfter", moveAfter) -} diff --git a/src/modules/delay/pf2e-hud.ts b/src/modules/delay/pf2e-hud.ts new file mode 100644 index 0000000..3259565 --- /dev/null +++ b/src/modules/delay/pf2e-hud.ts @@ -0,0 +1,70 @@ +import MODULE from "src" +import type { EncounterPF2e } from "types/pf2e/module/encounter" +import { isDelaying } from "./utils" +import { tryDelay } from "." + +export function onRenderPF2eHudTracker(app, tracker: HTMLElement) { + if (!MODULE.settings.modifyPF2eHud) return + + const combat = app.combat as EncounterPF2e + if (!combat?.started) return + + const t = tracker.querySelectorAll("ol.combatants li.combatant") + + for (const el of tracker.querySelectorAll("ol.combatants li.combatant")) { + const id = el.dataset.combatantId + if (!id) return + const c = combat.combatants.get(id) + if (!c || !c.isOwner || c.initiative == null) return + + let delayElement: HTMLElement | null = null + if (game.user.isGM) delayElement = el.querySelector("a.delay") + else + delayElement = + el.querySelector("i.fa-solid.fa-hourglass-start") ?? + el.querySelector("i.fa-solid.fa-dice-d20") + + if (!delayElement) return + + if (combat.combatant === c) { + delayElement.replaceWith(makeDelayButton()) + } else if (c.actor && isDelaying(c.actor)) { + delayElement.replaceWith(makeReturnButton()) + } else { + const icon = document.createElement("i") + icon.classList.add("fa-solid", "fa-dice-d20") + delayElement.replaceWith(icon) + } + } +} + +function parseHTML(html: string) { + return new DOMParser().parseFromString(html, "text/html").body.firstChild! +} + +function makeDelayButton() { + const node = parseHTML(` + + + + `) + node?.addEventListener("click", (e) => { + e.stopPropagation() + tryDelay() + }) + return node +} + +function makeReturnButton() { + if (!MODULE.settings.allowReturn) { + return parseHTML(``) + } + + const node = parseHTML(` + + + + `) + + return node +} diff --git a/src/modules/delay/tracker.ts b/src/modules/delay/tracker.ts new file mode 100644 index 0000000..b66119d --- /dev/null +++ b/src/modules/delay/tracker.ts @@ -0,0 +1,48 @@ +import type { CombatantPF2e } from "types/pf2e/module/encounter" +import MODULE from "src" +import { isDelaying } from "./utils" +import { tryDelay, tryReturn } from "." + +export function onRenderCombatTracker(tracker, html: JQuery, data) { + if (!MODULE.settings.showInCombatTracker) return + const combat = game.combat + if (!combat || !combat.started) return + + html.find(".combatant.actor").each((i, e) => { + const id = e.dataset.combatantId + if (!id) return + const c = combat.combatants.get(id) + if (!c || !c.isOwner || c.initiative == null) return + + if (combat.combatant?.id === c.id) drawButton("delay", $(e), c) + else if (c.actor && isDelaying(c.actor)) drawButton("return", $(e), c) + }) +} + +function drawButton(type: "delay" | "return", combatentHtml: JQuery, combatant: CombatantPF2e) { + let button = $(` +
+ +
+ `) + if (type === "return") { + const title = MODULE.settings.allowReturn ? "Return to initiative" : "Delaying" + const cls = MODULE.settings.allowReturn ? "initiative-return" : "initiative-delay-indicator" + button = $(` +
+ + +
+ `) + } + + const div = combatentHtml.find(".token-initiative") + div.find(".initiative").hide() + div.append(button) + + button.on("click", (e) => { + e.stopPropagation() + if (type === "delay") tryDelay() + else if (MODULE.settings.allowReturn) tryReturn(combatant) + }) +} diff --git a/src/modules/delay/utils.ts b/src/modules/delay/utils.ts new file mode 100644 index 0000000..c2f16eb --- /dev/null +++ b/src/modules/delay/utils.ts @@ -0,0 +1,44 @@ +import type { ActorPF2e } from "types/pf2e/module/actor" +import type { EncounterPF2e, RolledCombatant } from "types/pf2e/module/encounter" + +export function isDelaying(actor: ActorPF2e) { + return actor.items.some((e) => e.slug === "x-delay") +} + +// https://github.com/foundryvtt/pf2e/blob/8ea3864e0394e944e6ccf5b65a8e31489c68505e/src/module/apps/sidebar/encounter-tracker.ts#L318 +export function setInitiativeFromDrop( + combat: EncounterPF2e, + newOrder: RolledCombatant>[], + dropped: RolledCombatant>, +): void { + const aboveDropped = newOrder.find((c) => newOrder.indexOf(c) === newOrder.indexOf(dropped) - 1) + const belowDropped = newOrder.find((c) => newOrder.indexOf(c) === newOrder.indexOf(dropped) + 1) + + const hasAboveAndBelow = !!aboveDropped && !!belowDropped + const hasAboveAndNoBelow = !!aboveDropped && !belowDropped + const hasBelowAndNoAbove = !aboveDropped && !!belowDropped + const aboveIsHigherThanBelow = + hasAboveAndBelow && belowDropped.initiative < aboveDropped.initiative + const belowIsHigherThanAbove = + hasAboveAndBelow && belowDropped.initiative < aboveDropped.initiative + const wasDraggedUp = + !!belowDropped && combat.getCombatantWithHigherInit(dropped, belowDropped) === belowDropped + const wasDraggedDown = !!aboveDropped && !wasDraggedUp + + // Set a new initiative intuitively, according to allegedly commonplace intuitions + dropped.initiative = + hasBelowAndNoAbove || (aboveIsHigherThanBelow && wasDraggedUp) + ? belowDropped.initiative + 1 + : hasAboveAndNoBelow || (belowIsHigherThanAbove && wasDraggedDown) + ? aboveDropped.initiative - 1 + : hasAboveAndBelow + ? belowDropped.initiative + : dropped.initiative + + const withSameInitiative = newOrder.filter((c) => c.initiative === dropped.initiative) + if (withSameInitiative.length > 1) { + for (let priority = 0; priority < withSameInitiative.length; priority++) { + withSameInitiative[priority].flags.pf2e.overridePriority[dropped.initiative] = priority + } + } +} diff --git a/src/emanation-dialog.ts b/src/modules/emanation/emanation-dialog.ts similarity index 98% rename from src/emanation-dialog.ts rename to src/modules/emanation/emanation-dialog.ts index ac045f9..7992eb7 100644 --- a/src/emanation-dialog.ts +++ b/src/modules/emanation/emanation-dialog.ts @@ -1,8 +1,7 @@ import type { EffectPF2e, SpellPF2e } from "types/pf2e/module/item" import type { TokenDocumentPF2e } from "types/pf2e/module/scene" -import { MODULE_ID } from "./constants" +import { MODULE_ID } from "src/constants" import { type EmanationRequestData, extractFlagData } from "./emanation" -import Module from "./index" export class EmanationRequestDialog extends Application { #request: EmanationRequestData diff --git a/src/emanation.ts b/src/modules/emanation/emanation.ts similarity index 87% rename from src/emanation.ts rename to src/modules/emanation/emanation.ts index 0fab5d4..6ff49c7 100644 --- a/src/emanation.ts +++ b/src/modules/emanation/emanation.ts @@ -1,10 +1,36 @@ import type { ChatMessagePF2e } from "types/pf2e/module/chat-message" import type { ItemPF2e } from "types/pf2e/module/item" import type { SpellPF2e, SpellSheetPF2e } from "types/pf2e/module/item/spell" -import { MODULE_ID } from "./constants" +import { MODULE_ID } from "src/constants" import { EmanationRequestDialog } from "./emanation-dialog" -import Module from "./index" -import module from "./index" +import MODULE from "src/index" +import { BaseModule } from "../base" + +export class EmanationModule extends BaseModule { + settingsKey = "emanation-automation" + + enable(): void { + if (!game.modules.get("lib-wrapper")?.active) return + + super.enable() + + this.registerHook("renderChatMessage", onChatMessage) + } + + onReady(): void { + this.registerWrapper( + 'CONFIG.Item.sheetClasses.spell["pf2e.SpellSheetPF2e"].cls.prototype._renderInner', + spellSheetRenderWrapper, + "WRAPPER", + ) + + this.registerWrapper( + 'CONFIG.Item.sheetClasses.spell["pf2e.SpellSheetPF2e"].cls.prototype.activateListeners', + spellSheetActivateListenersWrapper, + "WRAPPER", + ) + } +} export interface EmanationRequestData { /** Spell UUID */ @@ -56,7 +82,7 @@ async function extractEffects(item: SpellPF2e) { } async function onChatMessage(msg: ChatMessagePF2e, html: JQuery<"div">) { - if (!module.settings.emanationAutomation) return + if (!MODULE.settings.emanationAutomation) return if (!game.user.isGM) return if (!msg.item || !msg.item.isOfType("spell")) return const spell = msg.item @@ -136,7 +162,7 @@ async function spellSheetRenderInner(sheet: SpellSheetPF2e, $html: JQuery) { async function spellSheetRenderWrapper(this: SpellSheetPF2e, wrapped, ...args) { const $html = await wrapped(...args) - if (module.settings.emanationAutomation) await spellSheetRenderInner(this, $html) + if (MODULE.settings.emanationAutomation) await spellSheetRenderInner(this, $html) return $html } @@ -163,24 +189,3 @@ function spellSheetActivateListenersWrapper(this: SpellSheetPF2e, wrapped, $html return $html } - -export function setupEmanationAutomation() { - if (!module.settings.emanationAutomation) return - - Hooks.on("renderChatMessage", onChatMessage) - // Hooks.on("renderSpellSheetPF2e", onSheetRender) - - libWrapper.register( - MODULE_ID, - 'CONFIG.Item.sheetClasses.spell["pf2e.SpellSheetPF2e"].cls.prototype._renderInner', - spellSheetRenderWrapper, - "WRAPPER", - ) - - libWrapper.register( - MODULE_ID, - 'CONFIG.Item.sheetClasses.spell["pf2e.SpellSheetPF2e"].cls.prototype.activateListeners', - spellSheetActivateListenersWrapper, - "WRAPPER", - ) -} diff --git a/src/modules/flat/flat.ts b/src/modules/flat/flat.ts new file mode 100644 index 0000000..641eaad --- /dev/null +++ b/src/modules/flat/flat.ts @@ -0,0 +1,118 @@ +import type { TokenPF2e } from "types/pf2e/module/canvas" +import MODULE from "src/index" +import { MoreDialog } from "./more-dialog" +import { BaseModule } from "../base" + +export class FlatModule extends BaseModule { + settingsKey = "show-global" + + enable() { + super.enable() + + this.registerHook("renderSidebarTab", onRenderSidebarTab) + this.registerHook("targetToken", onTargetToken) + } + disable() { + super.disable() + } +} + +export async function rollFlatCheck( + dc: number, + { hidden = false, label }: { hidden: boolean; label?: string }, +) { + const r = await new Roll("d20").roll() + const degree = r.total >= dc ? "success" : "failure" + const delta = r.total - dc + const deltaText = delta > 0 ? `+${delta}` : delta.toString() + + const flavor = document.createElement("span") + flavor.classList.add("flavor-text") + flavor.innerHTML = ` +
+
${label ?? "Flat"} DC: ${dc}
+
+ Result: ${degree.capitalize()} + by ${deltaText} +
+
+ ` + + r.toMessage({ flavor: flavor.outerHTML }, { rollMode: hidden ? "blindroll" : "roll" }) +} + +export const CONDITION_DCS = { + concealed: 5, + hidden: 11, + invisible: 11, +} + +function dcForToken(token: TokenPF2e) { + let dc = 0 + token.actor?.conditions.stored.forEach((c) => { + dc = Math.max(CONDITION_DCS[c.slug] ?? 0, dc) + }) + return dc || null +} + +async function rollForSingleTarget( + target: TokenPF2e | undefined, + { hidden = false }: { hidden: boolean }, +) { + if (!target) return + const dc = dcForToken(target) + if (!dc) ui.notifications.warn("Selected target has no conditions that require a flat check") + else rollFlatCheck(dc, { hidden }) +} + +async function onRenderSidebarTab(app: SidebarTab, html: HTMLCollection) { + if (app.tabName !== "chat") return + if (!MODULE.settings.fcButtonsEnabled) return + + const chat = html[0].querySelector("#chat-form") + + const template = await renderTemplate("modules/pf2e-flatcheck-helper/templates/buttons.hbs", {}) + const node = document.createElement("div") + node.id = "fc-container" + node.innerHTML = template + + chat?.after(node) + const $node = $(node) + // @ts-expect-error + $node.find(".tooltip").tooltipster({ + contentAsHTML: true, + }) + + node.querySelectorAll("button").forEach((button) => + button.addEventListener("click", function (e) { + const value = this.dataset.dc + if (!value) throw new Error(`Bad button DC value ${value}`) + const hidden = e.ctrlKey + + if (value === "targets") { + if (game.user?.targets.size === 0) return ui.notifications.warn("No targets selected") + if (game.user?.targets.size === 1) + return rollForSingleTarget(game.user.targets.first(), { hidden }) + else return ui.notifications.warn("Too many targets") + } else if (value === "more") { + new MoreDialog().render(true) + } else { + const dc = Number(value) + if (Number.isNaN(dc)) throw new Error(`Bad button DC value ${value}`) + + rollFlatCheck(dc, { hidden }) + } + }), + ) +} + +function onTargetToken(user: User) { + if (user.id !== game.user?.id) return + if (game.user.targets.size !== 1) + return document.querySelector("#fc-button-target")?.classList.remove("highlight") + const effectSlugs = Object.keys(CONDITION_DCS) + + if (game.user?.targets?.first()?.actor?.conditions.some((c) => effectSlugs.includes(c.slug))) + document.querySelector("#fc-button-target")?.classList.add("highlight") + else document.querySelector("#fc-button-target")?.classList.remove("highlight") +} diff --git a/src/more-dialog.ts b/src/modules/flat/more-dialog.ts similarity index 100% rename from src/more-dialog.ts rename to src/modules/flat/more-dialog.ts diff --git a/src/modules/life-link.ts b/src/modules/life-link.ts new file mode 100644 index 0000000..1a74725 --- /dev/null +++ b/src/modules/life-link.ts @@ -0,0 +1,326 @@ +import type { ActorPF2e } from "types/pf2e/module/actor" +import type { ChatMessagePF2e } from "types/pf2e/module/chat-message" +import type { CombatantPF2e, EncounterPF2e } from "types/pf2e/module/encounter" +import type { EffectPF2e, ItemPF2e } from "types/pf2e/module/item" +import MODULE from "src/index" +import { actorEffectBySlug, actorHasEffect } from "src/utils" +import { BaseModule } from "./base" + +export class LifeLinkModule extends BaseModule { + settingsKey = "lifelink" + + enable() { + super.enable() + + this.registerHook("pf2e.startTurn", onStartTurn) + this.registerHook("createItem", onCreateItem) + this.registerHook("createChatMessage", onCreateMessage) + this.registerHook("renderChatMessage", onRenderChatMessage) + } +} + +interface ButtonArgs { + // HP to transfer from source to target + // Stops if source doesn't have enough HP + transfer?: number + // HP to restore to target + heal?: number + // HP to remove from source + dmg?: number + source: string + target: string + cd?: 1 +} + +const UNDO_BUTTON_MARKUP = `` + +function makeButton(label: string, args: ButtonArgs) { + const json = JSON.stringify(args) + return ` + + + ${label} + + ` +} + +async function updateHP(actor, delta) { + const hp = actor.system.attributes.hp.value + await actor.update({ + "system.attributes.hp.value": hp + delta, + }) +} + +async function handleTransferButton(args: ButtonArgs) { + const required = ["source", "target"] + for (const k of required) { + if (!(k in args)) return ui.notifications.error(`Missing arg ${k}`) + } + + const source = fromUuidSync(args.source) + const target = fromUuidSync(args.target) + if (!target) return ui.notifications.error("No target actor") + if (!source) return ui.notifications.error("No source actor") + if (source.id === target.id) return ui.notifications.error("Can't transfer damage to self!") + + let transfer = 0 + + if (args.transfer) { + const missingHP = + // @ts-expect-error pf2e + target.system.attributes.hp.max - target.system.attributes.hp.value + const maxTransfer = Math.min(Number(args.transfer), missingHP) + + // @ts-expect-error pf2e + const hpRemaining = source.system.attributes.hp.value + transfer = Math.min(maxTransfer, hpRemaining) + + if (transfer <= 0) return ui.notifications.warn("No HP remaining to transfer.") + } + + let heal = transfer + let dmg = transfer + + // If the HP transfer (from life link) reduces the source to 0, share life no longer applies + // @ts-expect-error pf2e + if (transfer < source.system.attributes.hp.value) { + heal += args.heal ?? 0 + dmg += args.dmg ?? 0 + } + await updateHP(source, -dmg) + await updateHP(target, heal) + + if (args.cd) { + await target.createEmbeddedDocuments("Item", [ + { + type: "effect", + name: "Life Link CD", + img: "systems/pf2e/icons/spells/life-link.webp", + system: { + tokenIcon: { show: true }, + duration: { + value: 1, + unit: "rounds", + sustained: false, + expiry: "turn-start", + }, + slug: "life-link-cd", + }, + }, + ]) + } + + // @ts-expect-error Using uuids as keys doesn't work, but this does. Only question is when does this break + await ChatMessage.create({ + content: ` + ${target.name} +${heal} HP + 🡰 + ${source.name} -${dmg} HP + + ${UNDO_BUTTON_MARKUP} + `, + flags: { + undo: [ + [source.uuid, dmg], + [target.uuid, -heal], + ], + }, + }) +} + +function handleSpiritLink(effect: EffectPF2e) { + const { actor, origin } = effect + + if (!actor) { + return null + } + + if (!origin || origin.id === actor.id) { + ui.notifications.error( + `Bad origin actor for Spirit Linked effect on ${actor.name}! See module readme.`, + ) + return null + } + + const transfer = effect.level * 2 + const missingHP = actor.system.attributes.hp!.max - actor.system.attributes.hp!.value + if (missingHP <= 0) return null + + return makeButton(`${transfer} HP to ${actor.name}`, { + transfer, + source: origin.uuid, + target: actor.uuid, + }) +} + +async function onStartTurn(combatant: CombatantPF2e) { + if (game?.users?.activeGM?.id !== game.user?.id) return + + const links: string[] = [] + + canvas.scene?.tokens.forEach(({ actor }) => { + if (!actor) return + const e = actorEffectBySlug(actor, "spirit-linked") + if (!e) return + if (combatant.actor?.id !== e.origin?.id) return + + const link = handleSpiritLink(e) + if (link) links.push(link) + }) + + const content = `Spirit Link
${links.join("
")}` + + if (links.length) { + await ChatMessage.create({ + content: content, + whisper: ChatMessage.getWhisperRecipients("GM").map((u) => u.id), + speaker: ChatMessage.getSpeaker({ actor: combatant.actor }), + }) + } +} + +async function onCreateItem(item: ItemPF2e) { + if (item.isOfType("effect") && item.slug === "spirit-linked") { + const link = handleSpiritLink(item) + if (link) { + await ChatMessage.create({ + content: `Spirit Link
${link}`, + whisper: ChatMessage.getWhisperRecipients("GM").map((u) => u.id), + speaker: ChatMessage.getSpeaker({ actor: item.actor }), + }) + } + } +} + +async function onCreateMessage(msg: ChatMessagePF2e) { + if (game.users?.activeGM?.id !== game.user?.id) return + + const flags = msg.flags?.pf2e?.appliedDamage + const uuid = flags?.uuid + const dmg = flags?.updates.find((e) => e.path === "system.attributes.hp.value")?.value + if (!uuid || !dmg || dmg <= 0) return + + const actor = fromUuidSync(uuid) as ActorPF2e + if (!actor) return + + let lifeLinkTransfer = 0 + + const lifeLinkEffect = actorEffectBySlug(actor, "life-linked") + if (lifeLinkEffect && !actorHasEffect(actor, "life-link-cd")) { + lifeLinkTransfer = (() => { + if (!lifeLinkEffect.origin || lifeLinkEffect.origin.id === actor.id) { + ui.notifications.error( + `Bad origin actor for Life Linked effect on ${actor.name}! See module readme.`, + { + permanent: true, + }, + ) + return 0 + } + + let maxTransfer = 3 + if (MODULE.settings.lifeLinkVariant === "plus") + maxTransfer = 2 + Math.floor((lifeLinkEffect.level - 1) / 2) * 3 + else { + if (lifeLinkEffect.level >= 3) maxTransfer = 5 + if (lifeLinkEffect.level >= 6) maxTransfer = 10 + if (lifeLinkEffect.level >= 9) maxTransfer = 15 + } + + return Math.min(maxTransfer, dmg) + })() + } + + const shareLifeEffect = actorEffectBySlug(actor, "share-life") + + if (shareLifeEffect && !shareLifeEffect?.origin) + ui.notifications.error( + `Bad origin actor for Share Life effect on ${actor.name}! See module readme.`, + { + permanent: true, + }, + ) + + const buttons: string[] = [] + ;(() => { + if (shareLifeEffect && lifeLinkTransfer) { + const remainingDmg = dmg - lifeLinkTransfer + + if ( + shareLifeEffect?.origin && + lifeLinkEffect?.origin && + shareLifeEffect.origin.uuid === lifeLinkEffect.origin.uuid + ) { + // Both effects from the same source -> One Button + buttons.push( + makeButton( + `${Math.ceil(remainingDmg / 2) + lifeLinkTransfer} to ${lifeLinkEffect.origin.name}`, + { + transfer: lifeLinkTransfer, + heal: remainingDmg === 1 ? 1 : Math.floor(remainingDmg / 2), + dmg: Math.ceil(remainingDmg / 2), + cd: 1, + source: lifeLinkEffect.origin.uuid, + target: actor.uuid, + }, + ), + ) + return + } + } + // return above means this is unreachable if both effects are from the same source + if (shareLifeEffect?.origin) { + const remainingDmg = dmg - lifeLinkTransfer + // Button for Share Life + if (remainingDmg) + buttons.push( + makeButton( + `(Share Life) ${Math.ceil(remainingDmg / 2)} to ${shareLifeEffect.origin.name}`, + { + heal: remainingDmg === 1 ? 1 : Math.floor(remainingDmg / 2), + dmg: Math.ceil(remainingDmg / 2), + source: shareLifeEffect.origin.uuid, + target: actor.uuid, + }, + ), + ) + } + if (lifeLinkEffect?.origin && lifeLinkTransfer) { + buttons.push( + makeButton(`(Life Link) ${lifeLinkTransfer} to ${lifeLinkEffect.origin.name}`, { + transfer: lifeLinkTransfer, + cd: 1, + source: lifeLinkEffect.origin.uuid, + target: actor.uuid, + }), + ) + } + })() + + if (buttons.length) { + await ChatMessage.create({ + content: `Damage Transfer
${buttons.join("
")}`, + whisper: ChatMessage.getWhisperRecipients("GM").map((u) => u.id), + speaker: ChatMessage.getSpeaker(actor), + }) + } +} + +function onRenderChatMessage(msg: ChatMessagePF2e, html: JQuery) { + if (!game.user?.isGM) return + html.find("a.life-link").on("click", async (event) => { + const args = JSON.parse(event.target.dataset.args!) as ButtonArgs + await handleTransferButton(args) + }) + html.find("button.fc-undo-button").on("click", async () => { + const data = msg.flags.undo as unknown as [string, number][] + for (const [uuid, dmg] of data) { + const actor = await fromUuid(uuid) + await updateHP(actor, dmg) + } + html.find(".undo-text").addClass("undo") + await msg.update({ + content: html.find(".message-content").html(), + }) + }) +} diff --git a/src/settings.ts b/src/settings.ts index 63e710f..f61e45a 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,5 +1,8 @@ import { MODULE_ID } from "./constants" +type Callback = (value: unknown) => void +const listeners: Record = {} + export const settings = { get fcButtonsEnabled() { return (game.settings.get(MODULE_ID, "show-global") && @@ -22,6 +25,9 @@ export const settings = { get showInTokenHUD() { return game.settings.get(MODULE_ID, "delay-token-hud") as boolean }, + get modifyPF2eHud() { + return game.settings.get(MODULE_ID, "pf2e-hud-enable") as boolean + }, get removeCombatToggle() { return game.settings.get(MODULE_ID, "token-hud-remove-combat-toggle") as boolean }, @@ -40,7 +46,7 @@ export const settings = { }, init() { - game.settings.register(MODULE_ID, "show-global", { + register("show-global", { name: "Enable flat check buttons", hint: "Global setting: Enables flat check buttons below the chat box.", scope: "world", @@ -49,7 +55,7 @@ export const settings = { type: Boolean, requiresReload: true, }) - game.settings.register(MODULE_ID, "show", { + register("show", { name: "Show flat check buttons", hint: "Client setting: Turn off to hide the flat check buttons just for you.", scope: "client", @@ -59,7 +65,7 @@ export const settings = { requiresReload: true, }) - game.settings.register(MODULE_ID, "delay-combat-tracker", { + register("delay-combat-tracker", { name: "Show delay button in combat tracker", hint: "Adds delay/return buttons to the combat tracker. Will probably not work with any modules that change the combat tracker.", scope: "world", @@ -68,7 +74,7 @@ export const settings = { type: Boolean, }) - game.settings.register(MODULE_ID, "delay-token-hud", { + register("delay-token-hud", { name: "Show delay button in token HUD", hint: "Adds delay/return buttons to the menu that appears when right-clicking a token", scope: "world", @@ -77,7 +83,7 @@ export const settings = { type: Boolean, }) - game.settings.register(MODULE_ID, "delay-return", { + register("delay-return", { name: "Enable return button", hint: "Allows returning to initiative by pressing the delay button again.", scope: "world", @@ -86,7 +92,7 @@ export const settings = { type: Boolean, }) - game.settings.register(MODULE_ID, "delay-prompt", { + register("delay-prompt", { name: "Prompt for new initiative", hint: "Lets the user select a combatant to delay their turn after. Can still return early anytime they want.", scope: "world", @@ -95,7 +101,7 @@ export const settings = { type: Boolean, }) - game.settings.register(MODULE_ID, "delay-create-message", { + register("delay-create-message", { name: "Delay/Return creates chat message", scope: "world", config: true, @@ -103,7 +109,7 @@ export const settings = { type: Boolean, }) - game.settings.register(MODULE_ID, "token-hud-remove-combat-toggle", { + register("token-hud-remove-combat-toggle", { name: "Remove combat toggle from token HUD", hint: "Removes the 'Toggle Combat State' button for tokens in combat", scope: "world", @@ -112,7 +118,16 @@ export const settings = { type: Boolean, }) - game.settings.register(MODULE_ID, "lifelink", { + register("pf2e-hud-enable", { + name: "Modify PF2e HUD", + hint: "Overrides PF2e HUDs delay handling with this modules implementation. Please report issue with this to me and not to PF2e HUD!", + scope: "world", + config: true, + default: false, + type: Boolean, + }) + + register("lifelink", { name: "Enable life/spirit link automation buttons", hint: "Check the module readme for setup steps.", scope: "world", @@ -121,7 +136,7 @@ export const settings = { type: Boolean, }) - game.settings.register(MODULE_ID, "lifelink-formular", { + register("lifelink-formular", { name: "Life Link Formular", hint: "Variant of life link damage absorption to use", scope: "world", @@ -134,7 +149,7 @@ export const settings = { }, }) - game.settings.register(MODULE_ID, "emanation-automation", { + register("emanation-automation", { name: "Enable automatic emanation effect application", hint: "Still experimental, may change it this works in the future. Requires libwrapper.", scope: "world", @@ -142,5 +157,31 @@ export const settings = { type: Boolean, default: false, }) + + for (const [key, setting] of game.settings.settings) { + if (!key.startsWith(MODULE_ID)) continue + } + }, + + addListener(key: string, callback: Callback) { + listeners[key] = callback + }, + removeListener(key: string) { + delete listeners[key] }, + callListener(key: string, value: unknown) { + listeners[key]?.(value) + }, +} + +type SettingRegistration = Parameters[2] + +function register(key: string, data: SettingRegistration) { + game.settings.register(MODULE_ID, key, { + ...data, + onChange() { + const value = game.settings.get(MODULE_ID, key) + settings.callListener(key, value) + }, + }) } diff --git a/src/types-modules.d.ts b/src/types-modules.d.ts index b03291b..76f03ea 100644 --- a/src/types-modules.d.ts +++ b/src/types-modules.d.ts @@ -7,6 +7,7 @@ declare global { type: "MIXED" | "WRAPPER" | "OVERRIDE", options?: any, ) + unregister(moduleId: string, target: number | string) } const libWrapper: LibWrapper diff --git a/src/vite-index.js b/src/vite-index.js new file mode 100644 index 0000000..7b8986e --- /dev/null +++ b/src/vite-index.js @@ -0,0 +1,2 @@ +import "./index.ts"; +import "./main.css"; diff --git a/tsconfig.json b/tsconfig.json index 3e0073c..67ba4a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,13 +2,9 @@ "compilerOptions": { "target": "ESNext", "module": "ESNext", - "lib": [ - "DOM", - "ESNext" - ], "allowJs": true, "outDir": "dist", - "rootDir": "./src", + "rootDir": ".", "noEmit": true, "strict": true, "noImplicitAny": false, @@ -18,6 +14,7 @@ "types", ], "types": [ + "node", "jquery", "foundry", "pf2e" @@ -27,11 +24,16 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "baseUrl": ".", + "paths": { + "src": [ + "./src/index" + ], + "src/*": [ + "./src/*" + ] + } }, "include": [ "./src/**/*", ], - "files": [ - "./src/index.ts" - ], } \ No newline at end of file diff --git a/types/pf2e/index.d.ts b/types/pf2e/index.d.ts index 99aed36..9c35746 100644 --- a/types/pf2e/index.d.ts +++ b/types/pf2e/index.d.ts @@ -1,332 +1,343 @@ -import type { ActorPF2e } from "types/pf2emodule/actor/index.ts" -import type { Action } from "types/pf2emodule/actor/actions/index.ts" -import type { AutomaticBonusProgression } from "types/pf2emodule/actor/character/automatic-bonus-progression.ts" -import type { ElementalBlast } from "types/pf2emodule/actor/character/elemental-blast.ts" -import type { FeatGroupOptions } from "types/pf2emodule/actor/character/feats.ts" -import type { CheckModifier, ModifierPF2e, ModifierType, StatisticModifier } from "types/pf2emodule/actor/modifiers.ts" -import type { ItemPF2e, PhysicalItemPF2e } from "types/pf2emodule/item/index.ts" -import type { ConditionSource } from "types/pf2emodule/item/condition/data.ts" -import type { CoinsPF2e } from "types/pf2emodule/item/physical/helpers.ts" -import type { ActiveEffectPF2e } from "types/pf2emodule/active-effect.ts" +import type { ActorPF2e } from "types/pf2e/module/actor/index.ts" +import type { Action } from "types/pf2e/module/actor/actions/index.ts" +import type { AutomaticBonusProgression } from "types/pf2e/module/actor/character/automatic-bonus-progression.ts" +import type { ElementalBlast } from "types/pf2e/module/actor/character/elemental-blast.ts" +import type { FeatGroupOptions } from "types/pf2e/module/actor/character/feats.ts" import type { - CompendiumBrowser, - CompendiumBrowserSettings, - CompendiumBrowserSources, -} from "types/pf2emodule/apps/compendium-browser/index.ts" -import type { EffectsPanel } from "types/pf2emodule/apps/effects-panel.ts" -import type { HotbarPF2e } from "types/pf2emodule/apps/hotbar.ts" -import type { LicenseViewer } from "types/pf2emodule/apps/license-viewer/app.ts" + CheckModifier, + ModifierPF2e, + ModifierType, + StatisticModifier, +} from "types/pf2e/module/actor/modifiers.ts" +import type { ItemPF2e, PhysicalItemPF2e } from "types/pf2e/module/item/index.ts" +import type { ConditionSource } from "types/pf2e/module/item/condition/data.ts" +import type { CoinsPF2e } from "types/pf2e/module/item/physical/helpers.ts" +import type { ActiveEffectPF2e } from "types/pf2e/module/active-effect.ts" import type { - ActorDirectoryPF2e, - ChatLogPF2e, - CompendiumDirectoryPF2e, - EncounterTrackerPF2e, -} from "types/pf2emodule/apps/sidebar/index.ts" -import type { WorldClock } from "types/pf2emodule/apps/world-clock/app.ts" -import type { CanvasPF2e, EffectsCanvasGroupPF2e } from "types/pf2emodule/canvas/index.ts" -import type { StatusEffects } from "types/pf2emodule/canvas/status-effects.ts" -import type { ChatMessagePF2e } from "types/pf2emodule/chat-message/index.ts" -import type { ActorsPF2e } from "types/pf2emodule/collection/actors.ts" -import type { CombatantPF2e, EncounterPF2e } from "types/pf2emodule/encounter/index.ts" -import type { MacroPF2e } from "types/pf2emodule/macro.ts" -import type { RuleElementPF2e, RuleElements } from "types/pf2emodule/rules/index.ts" -import type { UserPF2e } from "types/pf2emodule/user/index.ts" + CompendiumBrowser, + CompendiumBrowserSettings, + CompendiumBrowserSources, +} from "types/pf2e/module/apps/compendium-browser/index.ts" +import type { EffectsPanel } from "types/pf2e/module/apps/effects-panel.ts" +import type { HotbarPF2e } from "types/pf2e/module/apps/hotbar.ts" +import type { LicenseViewer } from "types/pf2e/module/apps/license-viewer/app.ts" import type { - AmbientLightDocumentPF2e, - MeasuredTemplateDocumentPF2e, - RegionBehaviorPF2e, - RegionDocumentPF2e, - ScenePF2e, - TileDocumentPF2e, - TokenDocumentPF2e, -} from "types/pf2emodule/scene/index.ts" -import type { ActorDeltaPF2e } from "types/pf2emodule/scene/token-document/actor-delta.ts" -import type { PF2ECONFIG, StatusEffectIconTheme } from "types/pf2escripts/config/index.ts" -import type { DicePF2e } from "types/pf2escripts/dice.ts" + ActorDirectoryPF2e, + ChatLogPF2e, + CompendiumDirectoryPF2e, + EncounterTrackerPF2e, +} from "types/pf2e/module/apps/sidebar/index.ts" +import type { WorldClock } from "types/pf2e/module/apps/world-clock/app.ts" +import type { CanvasPF2e, EffectsCanvasGroupPF2e } from "types/pf2e/module/canvas/index.ts" +import type { StatusEffects } from "types/pf2e/module/canvas/status-effects.ts" +import type { ChatMessagePF2e } from "types/pf2e/module/chat-message/index.ts" +import type { ActorsPF2e } from "types/pf2e/module/collection/actors.ts" +import type { CombatantPF2e, EncounterPF2e } from "types/pf2e/module/encounter/index.ts" +import type { MacroPF2e } from "types/pf2e/module/macro.ts" +import type { RuleElementPF2e, RuleElements } from "types/pf2e/module/rules/index.ts" +import type { UserPF2e } from "types/pf2e/module/user/index.ts" import type { - calculateXP, - checkPrompt, - editPersistent, - launchTravelSheet, - perceptionForSelected, - rollActionMacro, - rollItemMacro, - stealthForSelected, - xpFromEncounter, -} from "types/pf2escripts/macros/index.ts" -import type { remigrate } from "types/pf2escripts/system/remigrate.ts" -import type { CheckPF2e } from "types/pf2emodule/system/check/index.ts" -import type { ConditionManager } from "types/pf2emodule/system/conditions/manager.ts" -import type { EffectTracker } from "types/pf2emodule/system/effect-tracker.ts" -import type { ModuleArt } from "types/pf2emodule/system/module-art.ts" -import type { Predicate } from "types/pf2emodule/system/predication.ts" + AmbientLightDocumentPF2e, + MeasuredTemplateDocumentPF2e, + RegionBehaviorPF2e, + RegionDocumentPF2e, + ScenePF2e, + TileDocumentPF2e, + TokenDocumentPF2e, +} from "types/pf2e/module/scene/index.ts" +import type { ActorDeltaPF2e } from "types/pf2e/module/scene/token-document/actor-delta.ts" +import type { PF2ECONFIG, StatusEffectIconTheme } from "types/pf2e/scripts/config/index.ts" +import type { DicePF2e } from "types/pf2e/scripts/dice.ts" import type { - CustomDamageData, - HomebrewTag, - HomebrewTraitSettingsKey, - LanguageSettings, -} from "types/pf2emodule/system/settings/homebrew/index.ts" -import type { TextEditorPF2e } from "types/pf2emodule/system/text-editor.ts" -import type { sluggify } from "types/pf2eutil/index.ts" + calculateXP, + checkPrompt, + editPersistent, + launchTravelSheet, + perceptionForSelected, + rollActionMacro, + rollItemMacro, + stealthForSelected, + xpFromEncounter, +} from "types/pf2e/scripts/macros/index.ts" +import type { remigrate } from "types/pf2e/scripts/system/remigrate.ts" +import type { CheckPF2e } from "types/pf2e/module/system/check/index.ts" +import type { ConditionManager } from "types/pf2e/module/system/conditions/manager.ts" +import type { EffectTracker } from "types/pf2e/module/system/effect-tracker.ts" +import type { ModuleArt } from "types/pf2e/module/system/module-art.ts" +import type { Predicate } from "types/pf2e/module/system/predication.ts" +import type { + CustomDamageData, + HomebrewTag, + HomebrewTraitSettingsKey, + LanguageSettings, +} from "types/pf2e/module/system/settings/homebrew/index.ts" +import type { TextEditorPF2e } from "types/pf2e/module/system/text-editor.ts" +import type { sluggify } from "types/pf2e/util/index.ts" interface GamePF2e - extends Game< - ActorPF2e, - ActorsPF2e>, - ChatMessagePF2e, - EncounterPF2e, - ItemPF2e, - MacroPF2e, - ScenePF2e, - UserPF2e - > { - pf2e: { - actions: Record & Collection - compendiumBrowser: CompendiumBrowser - licenseViewer: LicenseViewer - worldClock: WorldClock - effectPanel: EffectsPanel - effectTracker: EffectTracker - rollActionMacro: typeof rollActionMacro - rollItemMacro: typeof rollItemMacro - gm: { - calculateXP: typeof calculateXP - checkPrompt: typeof checkPrompt - editPersistent: typeof editPersistent - launchTravelSheet: typeof launchTravelSheet - perceptionForSelected: typeof perceptionForSelected - stealthForSelected: typeof stealthForSelected - xpFromEncounter: typeof xpFromEncounter - } - system: { - moduleArt: ModuleArt - remigrate: typeof remigrate - sluggify: typeof sluggify - generateItemName: (item: PhysicalItemPF2e) => string - } - variantRules: { - AutomaticBonusProgression: typeof AutomaticBonusProgression - } - Check: typeof CheckPF2e - CheckModifier: typeof CheckModifier - Coins: typeof CoinsPF2e - ConditionManager: typeof ConditionManager - Dice: typeof DicePF2e - ElementalBlast: typeof ElementalBlast - Modifier: typeof ModifierPF2e - ModifierType: { - [K in Uppercase]: Lowercase - } - Predicate: typeof Predicate - RuleElement: typeof RuleElementPF2e - RuleElements: typeof RuleElements - StatisticModifier: typeof StatisticModifier - StatusEffects: typeof StatusEffects - TextEditor: typeof TextEditorPF2e - /** Cached values of frequently-checked settings */ - settings: { - automation: { - /** Flanking detection */ - flanking: boolean - } - /** Campaign feat slots */ - campaign: { - feats: { - enabled: boolean - sections: FeatGroupOptions[] - } - languages: LanguageSettings - } - critFumble: { - buttons: boolean - cards: boolean - } - dragMeasurement: "always" | "encounters" | "never" - /** Encumbrance automation */ - encumbrance: boolean - gmVision: boolean - /** Immunities, weaknesses, and resistances */ - iwr: boolean - metagame: { - breakdowns: boolean - dcs: boolean - secretChecks: boolean - partyStats: boolean - partyVision: boolean - results: boolean - } - /** Rules-based vision */ - rbv: boolean - tokens: { - /** Automatic scaling of tokens belong to small actor */ - autoscale: boolean - /** Token nameplate visibility sets name visibility in encounter tracker */ - nameVisibility: boolean - /** Nath Mode */ - nathMode: boolean - } - /** Theater-of-the-mind toggles */ - totm: boolean - /** Variant urles */ - variants: { - /** Automatic Bonus Progression */ - abp: "noABP" | "ABPFundamentalPotency" | "ABPRulesAsWritten" - /** Free Archetype */ - fa: boolean - /** Gradual Ability Boosts */ - gab: boolean - /** Proficiency without Level */ - pwol: { - enabled: boolean - /** Modifiers for each proficiency rank */ - modifiers: [number, number, number, number, number] - } - /** Stamina */ - stamina: boolean - } - } - } + extends Game< + ActorPF2e, + ActorsPF2e>, + ChatMessagePF2e, + EncounterPF2e, + ItemPF2e, + MacroPF2e, + ScenePF2e, + UserPF2e + > { + pf2e: { + actions: Record & Collection + compendiumBrowser: CompendiumBrowser + licenseViewer: LicenseViewer + worldClock: WorldClock + effectPanel: EffectsPanel + effectTracker: EffectTracker + rollActionMacro: typeof rollActionMacro + rollItemMacro: typeof rollItemMacro + gm: { + calculateXP: typeof calculateXP + checkPrompt: typeof checkPrompt + editPersistent: typeof editPersistent + launchTravelSheet: typeof launchTravelSheet + perceptionForSelected: typeof perceptionForSelected + stealthForSelected: typeof stealthForSelected + xpFromEncounter: typeof xpFromEncounter + } + system: { + moduleArt: ModuleArt + remigrate: typeof remigrate + sluggify: typeof sluggify + generateItemName: (item: PhysicalItemPF2e) => string + } + variantRules: { + AutomaticBonusProgression: typeof AutomaticBonusProgression + } + Check: typeof CheckPF2e + CheckModifier: typeof CheckModifier + Coins: typeof CoinsPF2e + ConditionManager: typeof ConditionManager + Dice: typeof DicePF2e + ElementalBlast: typeof ElementalBlast + Modifier: typeof ModifierPF2e + ModifierType: { + [K in Uppercase]: Lowercase + } + Predicate: typeof Predicate + RuleElement: typeof RuleElementPF2e + RuleElements: typeof RuleElements + StatisticModifier: typeof StatisticModifier + StatusEffects: typeof StatusEffects + TextEditor: typeof TextEditorPF2e + /** Cached values of frequently-checked settings */ + settings: { + automation: { + /** Flanking detection */ + flanking: boolean + } + /** Campaign feat slots */ + campaign: { + feats: { + enabled: boolean + sections: FeatGroupOptions[] + } + languages: LanguageSettings + } + critFumble: { + buttons: boolean + cards: boolean + } + dragMeasurement: "always" | "encounters" | "never" + /** Encumbrance automation */ + encumbrance: boolean + gmVision: boolean + /** Immunities, weaknesses, and resistances */ + iwr: boolean + metagame: { + breakdowns: boolean + dcs: boolean + secretChecks: boolean + partyStats: boolean + partyVision: boolean + results: boolean + } + /** Rules-based vision */ + rbv: boolean + tokens: { + /** Automatic scaling of tokens belong to small actor */ + autoscale: boolean + /** Token nameplate visibility sets name visibility in encounter tracker */ + nameVisibility: boolean + /** Nath Mode */ + nathMode: boolean + } + /** Theater-of-the-mind toggles */ + totm: boolean + /** Variant urles */ + variants: { + /** Automatic Bonus Progression */ + abp: "noABP" | "ABPFundamentalPotency" | "ABPRulesAsWritten" + /** Free Archetype */ + fa: boolean + /** Gradual Ability Boosts */ + gab: boolean + /** Proficiency without Level */ + pwol: { + enabled: boolean + /** Modifiers for each proficiency rank */ + modifiers: [number, number, number, number, number] + } + /** Stamina */ + stamina: boolean + } + } + } } type ConfiguredConfig = Config< - AmbientLightDocumentPF2e, - ActiveEffectPF2e, - ActorPF2e, - ActorDeltaPF2e, - ChatLogPF2e, - ChatMessagePF2e, - EncounterPF2e, - CombatantPF2e, - EncounterTrackerPF2e, - CompendiumDirectoryPF2e, - HotbarPF2e, - ItemPF2e, - MacroPF2e, - MeasuredTemplateDocumentPF2e, - RegionDocumentPF2e, - RegionBehaviorPF2e, - TileDocumentPF2e, - TokenDocumentPF2e, - WallDocument, - ScenePF2e, - UserPF2e, - EffectsCanvasGroupPF2e + AmbientLightDocumentPF2e, + ActiveEffectPF2e, + ActorPF2e, + ActorDeltaPF2e, + ChatLogPF2e, + ChatMessagePF2e, + EncounterPF2e, + CombatantPF2e, + EncounterTrackerPF2e, + CompendiumDirectoryPF2e, + HotbarPF2e, + ItemPF2e, + MacroPF2e, + MeasuredTemplateDocumentPF2e, + RegionDocumentPF2e, + RegionBehaviorPF2e, + TileDocumentPF2e, + TokenDocumentPF2e, + WallDocument, + ScenePF2e, + UserPF2e, + EffectsCanvasGroupPF2e > declare global { - interface ConfigPF2e extends ConfiguredConfig { - debug: ConfiguredConfig["debug"] & { - ruleElement: boolean - } - PF2E: typeof PF2ECONFIG - time: { - roundTime: number - } - } - const CONFIG: ConfigPF2e - const canvas: CanvasPF2e - namespace globalThis { - var game: GamePF2e - var fu: typeof foundry.utils - var ui: FoundryUI< - ActorDirectoryPF2e, - ItemDirectory>, - ChatLogPF2e, - CompendiumDirectoryPF2e, - EncounterTrackerPF2e, - HotbarPF2e - > - interface Math { - eq: (a: number, b: number) => boolean - gt: (a: number, b: number) => boolean - gte: (a: number, b: number) => boolean - lt: (a: number, b: number) => boolean - lte: (a: number, b: number) => boolean - ne: (a: number, b: number) => boolean - ternary: (condition: boolean | number, ifTrue: number, ifFalse: number) => number - } - } - interface Window { - AutomaticBonusProgression: typeof AutomaticBonusProgression - } - interface ClientSettings { - get(module: "pf2e", setting: "automation.actorsDeadAtZero"): "neither" | "npcsOnly" | "pcsOnly" | "both" - get(module: "pf2e", setting: "automation.effectExpiration"): boolean - get(module: "pf2e", setting: "automation.encumbrance"): boolean - get(module: "pf2e", setting: "automation.flankingDetection"): boolean - get(module: "pf2e", setting: "automation.iwr"): boolean - get(module: "pf2e", setting: "automation.lootableNPCs"): boolean - get(module: "pf2e", setting: "automation.removeExpiredEffects"): boolean - get(module: "pf2e", setting: "automation.rulesBasedVision"): boolean - get(module: "pf2e", setting: "gradualBoostsVariant"): boolean - get(module: "pf2e", setting: "automaticBonusVariant"): "noABP" | "ABPFundamentalPotency" | "ABPRulesAsWritten" - get(module: "pf2e", setting: "freeArchetypeVariant"): boolean - get(module: "pf2e", setting: "proficiencyVariant"): boolean - get(module: "pf2e", setting: "staminaVariant"): boolean - get(module: "pf2e", setting: "proficiencyUntrainedModifier"): number - get(module: "pf2e", setting: "proficiencyTrainedModifier"): number - get(module: "pf2e", setting: "proficiencyExpertModifier"): number - get(module: "pf2e", setting: "proficiencyMasterModifier"): number - get(module: "pf2e", setting: "proficiencyLegendaryModifier"): number - get(module: "pf2e", setting: "metagame_partyVision"): boolean - get(module: "pf2e", setting: "metagame_secretCondition"): boolean - get(module: "pf2e", setting: "metagame_secretDamage"): boolean - get(module: "pf2e", setting: "metagame_showBreakdowns"): boolean - get(module: "pf2e", setting: "metagame_showDC"): boolean - get(module: "pf2e", setting: "metagame_showPartyStats"): boolean - get(module: "pf2e", setting: "metagame_showResults"): boolean - get(module: "pf2e", setting: "metagame_tokenSetsNameVisibility"): boolean - get(module: "pf2e", setting: "metagame_secretChecks"): boolean - get(module: "pf2e", setting: "tokens.autoscale"): boolean - get(module: "pf2e", setting: "worldClock.dateTheme"): "AR" | "IC" | "AD" | "CE" - get(module: "pf2e", setting: "worldClock.playersCanView"): boolean - get(module: "pf2e", setting: "worldClock.showClockButton"): boolean - get(module: "pf2e", setting: "worldClock.syncDarkness"): boolean - get(module: "pf2e", setting: "worldClock.timeConvention"): 24 | 12 - get(module: "pf2e", setting: "worldClock.worldCreatedOn"): string - get(module: "pf2e", setting: "campaignFeats"): boolean - get(module: "pf2e", setting: "campaignFeatSections"): FeatGroupOptions[] - get(module: "pf2e", setting: "campaignType"): string - get(module: "pf2e", setting: "activeParty"): string - get(module: "pf2e", setting: "activePartyFolderState"): boolean - get(module: "pf2e", setting: "createdFirstParty"): boolean - get(module: "pf2e", setting: "homebrew.languages"): HomebrewTag<"languages">[] - get(module: "pf2e", setting: "homebrew.weaponCategories"): HomebrewTag<"weaponCategories">[] - get(module: "pf2e", setting: HomebrewTraitSettingsKey): HomebrewTag[] - get(module: "pf2e", setting: "homebrew.damageTypes"): CustomDamageData[] - get(module: "pf2e", setting: "homebrew.languageRarities"): LanguageSettings - get(module: "pf2e", setting: "compendiumBrowserPacks"): CompendiumBrowserSettings - get(module: "pf2e", setting: "compendiumBrowserSources"): CompendiumBrowserSources - get(module: "pf2e", setting: "critFumbleButtons"): boolean - get(module: "pf2e", setting: "critRule"): "doubledamage" | "doubledice" - get(module: "pf2e", setting: "deathIcon"): ImageFilePath - get(module: "pf2e", setting: "dragMeasurement"): "always" | "encounters" | "never" - get(module: "pf2e", setting: "drawCritFumble"): boolean - get(module: "pf2e", setting: "gmVision"): boolean - get(module: "pf2e", setting: "identifyMagicNotMatchingTraditionModifier"): 0 | 2 | 5 | 10 - get(module: "pf2e", setting: "minimumRulesUI"): Exclude - get(module: "pf2e", setting: "nathMode"): boolean - get(module: "pf2e", setting: "seenRemasterJournalEntry"): boolean - get(module: "pf2e", setting: "statusEffectType"): StatusEffectIconTheme - get(module: "pf2e", setting: "totmToggles"): boolean - get(module: "pf2e", setting: "worldSchemaVersion"): number - get(module: "pf2e", setting: "worldSystemVersion"): string - } - interface ClientSettingsMap { - get(key: "pf2e.worldClock.worldCreatedOn"): SettingConfig & { - default: string - } - } - interface RollMathProxy { - eq: (a: number, b: number) => boolean - gt: (a: number, b: number) => boolean - gte: (a: number, b: number) => boolean - lt: (a: number, b: number) => boolean - lte: (a: number, b: number) => boolean - ne: (a: number, b: number) => boolean - ternary: (condition: boolean | number, ifTrue: number, ifFalse: number) => number - } - const BUILD_MODE: "development" | "production" - const CONDITION_SOURCES: ConditionSource[] - const EN_JSON: typeof EnJSON - const ROLL_PARSER: string - const UUID_REDIRECTS: Record + interface ConfigPF2e extends ConfiguredConfig { + debug: ConfiguredConfig["debug"] & { + ruleElement: boolean + } + PF2E: typeof PF2ECONFIG + time: { + roundTime: number + } + } + const CONFIG: ConfigPF2e + const canvas: CanvasPF2e + namespace globalThis { + var game: GamePF2e + var fu: typeof foundry.utils + var ui: FoundryUI< + ActorDirectoryPF2e, + ItemDirectory>, + ChatLogPF2e, + CompendiumDirectoryPF2e, + EncounterTrackerPF2e, + HotbarPF2e + > + interface Math { + eq: (a: number, b: number) => boolean + gt: (a: number, b: number) => boolean + gte: (a: number, b: number) => boolean + lt: (a: number, b: number) => boolean + lte: (a: number, b: number) => boolean + ne: (a: number, b: number) => boolean + ternary: (condition: boolean | number, ifTrue: number, ifFalse: number) => number + } + } + interface Window { + AutomaticBonusProgression: typeof AutomaticBonusProgression + } + interface ClientSettings { + get( + module: "pf2e", + setting: "automation.actorsDeadAtZero", + ): "neither" | "npcsOnly" | "pcsOnly" | "both" + get(module: "pf2e", setting: "automation.effectExpiration"): boolean + get(module: "pf2e", setting: "automation.encumbrance"): boolean + get(module: "pf2e", setting: "automation.flankingDetection"): boolean + get(module: "pf2e", setting: "automation.iwr"): boolean + get(module: "pf2e", setting: "automation.lootableNPCs"): boolean + get(module: "pf2e", setting: "automation.removeExpiredEffects"): boolean + get(module: "pf2e", setting: "automation.rulesBasedVision"): boolean + get(module: "pf2e", setting: "gradualBoostsVariant"): boolean + get( + module: "pf2e", + setting: "automaticBonusVariant", + ): "noABP" | "ABPFundamentalPotency" | "ABPRulesAsWritten" + get(module: "pf2e", setting: "freeArchetypeVariant"): boolean + get(module: "pf2e", setting: "proficiencyVariant"): boolean + get(module: "pf2e", setting: "staminaVariant"): boolean + get(module: "pf2e", setting: "proficiencyUntrainedModifier"): number + get(module: "pf2e", setting: "proficiencyTrainedModifier"): number + get(module: "pf2e", setting: "proficiencyExpertModifier"): number + get(module: "pf2e", setting: "proficiencyMasterModifier"): number + get(module: "pf2e", setting: "proficiencyLegendaryModifier"): number + get(module: "pf2e", setting: "metagame_partyVision"): boolean + get(module: "pf2e", setting: "metagame_secretCondition"): boolean + get(module: "pf2e", setting: "metagame_secretDamage"): boolean + get(module: "pf2e", setting: "metagame_showBreakdowns"): boolean + get(module: "pf2e", setting: "metagame_showDC"): boolean + get(module: "pf2e", setting: "metagame_showPartyStats"): boolean + get(module: "pf2e", setting: "metagame_showResults"): boolean + get(module: "pf2e", setting: "metagame_tokenSetsNameVisibility"): boolean + get(module: "pf2e", setting: "metagame_secretChecks"): boolean + get(module: "pf2e", setting: "tokens.autoscale"): boolean + get(module: "pf2e", setting: "worldClock.dateTheme"): "AR" | "IC" | "AD" | "CE" + get(module: "pf2e", setting: "worldClock.playersCanView"): boolean + get(module: "pf2e", setting: "worldClock.showClockButton"): boolean + get(module: "pf2e", setting: "worldClock.syncDarkness"): boolean + get(module: "pf2e", setting: "worldClock.timeConvention"): 24 | 12 + get(module: "pf2e", setting: "worldClock.worldCreatedOn"): string + get(module: "pf2e", setting: "campaignFeats"): boolean + get(module: "pf2e", setting: "campaignFeatSections"): FeatGroupOptions[] + get(module: "pf2e", setting: "campaignType"): string + get(module: "pf2e", setting: "activeParty"): string + get(module: "pf2e", setting: "activePartyFolderState"): boolean + get(module: "pf2e", setting: "createdFirstParty"): boolean + get(module: "pf2e", setting: "homebrew.languages"): HomebrewTag<"languages">[] + get(module: "pf2e", setting: "homebrew.weaponCategories"): HomebrewTag<"weaponCategories">[] + get(module: "pf2e", setting: HomebrewTraitSettingsKey): HomebrewTag[] + get(module: "pf2e", setting: "homebrew.damageTypes"): CustomDamageData[] + get(module: "pf2e", setting: "homebrew.languageRarities"): LanguageSettings + get(module: "pf2e", setting: "compendiumBrowserPacks"): CompendiumBrowserSettings + get(module: "pf2e", setting: "compendiumBrowserSources"): CompendiumBrowserSources + get(module: "pf2e", setting: "critFumbleButtons"): boolean + get(module: "pf2e", setting: "critRule"): "doubledamage" | "doubledice" + get(module: "pf2e", setting: "deathIcon"): ImageFilePath + get(module: "pf2e", setting: "dragMeasurement"): "always" | "encounters" | "never" + get(module: "pf2e", setting: "drawCritFumble"): boolean + get(module: "pf2e", setting: "gmVision"): boolean + get(module: "pf2e", setting: "identifyMagicNotMatchingTraditionModifier"): 0 | 2 | 5 | 10 + get(module: "pf2e", setting: "minimumRulesUI"): Exclude + get(module: "pf2e", setting: "nathMode"): boolean + get(module: "pf2e", setting: "seenRemasterJournalEntry"): boolean + get(module: "pf2e", setting: "statusEffectType"): StatusEffectIconTheme + get(module: "pf2e", setting: "totmToggles"): boolean + get(module: "pf2e", setting: "worldSchemaVersion"): number + get(module: "pf2e", setting: "worldSystemVersion"): string + } + interface ClientSettingsMap { + get(key: "pf2e.worldClock.worldCreatedOn"): SettingConfig & { + default: string + } + } + interface RollMathProxy { + eq: (a: number, b: number) => boolean + gt: (a: number, b: number) => boolean + gte: (a: number, b: number) => boolean + lt: (a: number, b: number) => boolean + lte: (a: number, b: number) => boolean + ne: (a: number, b: number) => boolean + ternary: (condition: boolean | number, ifTrue: number, ifFalse: number) => number + } + const BUILD_MODE: "development" | "production" + const CONDITION_SOURCES: ConditionSource[] + const EN_JSON: typeof EnJSON + const ROLL_PARSER: string + const UUID_REDIRECTS: Record } export {} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..8f9f1b9 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,103 @@ +/* eslint-env node */ +import fs from "node:fs" +import { type Connect, type PluginOption, defineConfig } from "vite" +import tsconfigPaths from "vite-tsconfig-paths" +import checker from "vite-plugin-checker" +import moduleJSON from "./module.json" with { type: "json" } + +const packagePath = `modules/${moduleJSON.id}` +// const { esmodules, styles } = moduleJSON + +const skippedFiles = [`${moduleJSON.id}.css`].map((f) => `dist/${f}`).join("|") + +export default defineConfig(({ command: _buildOrServe }) => ({ + root: "src", + base: `/${packagePath}/dist`, + cacheDir: "../.vite-cache", + publicDir: "../assets", + + clearScreen: true, + + esbuild: { + target: ["es2022"], + }, + + resolve: { conditions: ["import", "browser"] }, + + server: { + open: false, + port: 30001, + proxy: { + // Serves static files from main Foundry server. + [`^(/${packagePath}/(assets|lang|packs|${skippedFiles}))`]: "http://localhost:30000", + + // All other paths besides package ID path are served from main Foundry server. + [`^(?!/${packagePath}/)`]: "http://localhost:30000", + + // Enable socket.io from main Foundry server. + "/socket.io": { target: "ws://localhost:30000", ws: true }, + }, + }, + + build: { + copyPublicDir: false, + outDir: "../dist", + emptyOutDir: true, + sourcemap: true, + minify: "terser" as const, + terserOptions: { + mangle: { + toplevel: true, + keep_classnames: true, + keep_fnames: true, + }, + module: true, + }, + lib: { + entry: "vite-index.js", + formats: ["es"], + fileName: "module", + }, + rollupOptions: { + output: { + // assetFileNames: assetInfo => (assetInfo.name === 'style.css') ? `${moduleJSON.id}.css` : (assetInfo.name as string), + }, + }, + }, + + optimizeDeps: { + esbuildOptions: { + target: "es2022", + }, + }, + + plugins: [ + /* process.env.IGNORE_CHECKER + ? undefined + : checker({ + typescript: true, + }), */ + tsconfigPaths(), + { + name: "change-names", + configureServer(server) { + server.middlewares.use((req: Connect.IncomingMessage & { url?: string }, res, next) => { + if (req.originalUrl === `/${packagePath}/dist/module.js`) { + req.url = `/${packagePath}/dist/vite-index.js` + } + next() + }) + }, + }, + { + name: "create-dist-files", + apply: "serve", + buildStart() { + const files = [...moduleJSON.esmodules, ...moduleJSON.styles] + for (const name of files) { + fs.writeFileSync(`${name}`, "", { flag: "a" }) + } + }, + }, + ], +})) diff --git a/yarn.lock b/yarn.lock index 7e6e8c1..a67ddf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -307,6 +307,13 @@ dependencies: "@types/sizzle" "*" +"@types/node@^22.3.0": + version "22.3.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.3.0.tgz#7f8da0e2b72c27c4f9bd3cb5ef805209d04d4f9e" + integrity sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g== + dependencies: + undici-types "~6.18.2" + "@types/sizzle@*": version "2.3.8" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627" @@ -444,6 +451,13 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +debug@^4.1.1: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + esbuild@^0.21.3: version "0.21.5" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" @@ -531,6 +545,11 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -604,6 +623,11 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -737,6 +761,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tsconfck@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.1.tgz#c7284913262c293b43b905b8b034f524de4a3162" + integrity sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ== + type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -747,6 +776,11 @@ typescript@*: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== +undici-types@~6.18.2: + version "6.18.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.18.2.tgz#8b678cf939d4fc9ec56be3c68ed69c619dee28b0" + integrity sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ== + universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" @@ -772,6 +806,15 @@ vite-plugin-checker@^0.7.2: vscode-languageserver-textdocument "^1.0.1" vscode-uri "^3.0.2" +vite-tsconfig-paths@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz#c9387a29c32fd586e4c7f4e2b2da1f0b5c9a7403" + integrity sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + vite@^5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.0.tgz#11dca8a961369ba8b5cae42d068c7ad684d5370f"