Repository: clausman/fantasy-statblocks
Pull request #2: Implement new importer for Pf2eTools format
Author: login: Copilot
Created: 2025-05-20T01:14:58Z | Merged: 2025-05-31T23:18:55Z

This PR adds a new importer to support the Pf2eTools JSON format, which is found in [Pf2eToolsOrg/Pf2eTools](https://github.com/Pf2eToolsOrg/Pf2eTools/tree/dev/data/bestiary).

### Implemented Changes:
- Created `src/importers/pf2eToolsImport.ts` with a dedicated importer for the Pf2eTools JSON format
- Added typings and interfaces for the Pf2eTools creature data structure
- Implemented conversion functions to properly map Pf2eTools data to the Monster interface
- Added special handling for traits, abilities, attacks, spellcasting, and other attributes
- Updated `importer.worker.ts` to support the new "pf2etools" source type
- Updated `index.ts` to export the new importer function

### How to use:
When importing monster data from Pf2eTools, select "pf2etools" as the source type in the import dialog.

This importer handles various aspects of the Pf2eTools format including:
- Core creature stats (AC, HP, abilities, etc.)
- Special abilities and actions with proper formatting
- Attacks and damage
- Spellcasting details
- Traits, resistances, immunities and weaknesses
- Skills and languages

Fixes #1.

---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Linked issue #1: Create a new importer for Pf2eTools
Implement a new importer similar to src/importers/pf2eMonsterToolImport.ts which import data of the format found in the files at https://github.com/Pf2eToolsOrg/Pf2eTools/tree/dev/data/bestiary

Diff:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..27bf3a0
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,21 @@
+name: Run Tests
+
+on:
+  pull_request:
+    branches: [ main ]
+  push:
+    branches: [ main ]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: 20
+          cache: 'npm'
+      - name: Install dependencies
+        run: npm ci
+      - name: Run tests
+        run: npm test
\ No newline at end of file
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 0000000..6a7b76d
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,18 @@
+/** @type {import('jest').Config} */
+const config = {
+  preset: 'ts-jest',
+  testEnvironment: 'node',
+  transform: {
+    '^.+\\.tsx?$': [
+      'ts-jest',
+      {
+        tsconfig: 'tsconfig-lib.json',
+      },
+    ],
+  },
+  moduleNameMapper: {
+    '^index$': '<rootDir>/index.ts',
+  },
+};
+
+module.exports = config;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index bea9783..5a3024e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
                 "@popperjs/core": "^2.11.8",
                 "@tsconfig/svelte": "^5.0.4",
                 "@types/dom-to-image": "^2.6.7",
+                "@types/jest": "^29.5.14",
                 "@types/node": "^22.5.4",
                 "@types/xmldom": "^0.1.34",
                 "@xmldom/xmldom": "^0.9.2",
@@ -53,6 +54,7 @@
                 "svelte-preprocess": "^6.0.2",
                 "svelte-select": "^5.8.3",
                 "title-case": "^4.3.1",
+                "ts-jest": "^29.3.4",
                 "tslib": "^2.7.0",
                 "tsup": "^8.2.4",
                 "typescript": "^5.6.2"
@@ -2509,6 +2511,17 @@
                 "@types/istanbul-lib-report": "*"
             }
         },
+        "node_modules/@types/jest": {
+            "version": "29.5.14",
+            "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz",
+            "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "expect": "^29.0.0",
+                "pretty-format": "^29.0.0"
+            }
+        },
         "node_modules/@types/json-schema": {
             "version": "7.0.15",
             "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2930,6 +2943,13 @@
                 "node": ">=0.10.0"
             }
         },
+        "node_modules/async": {
+            "version": "3.2.6",
+            "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+            "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/axobject-query": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3218,6 +3238,19 @@
                 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
             }
         },
+        "node_modules/bs-logger": {
+            "version": "0.2.6",
+            "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
+            "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "fast-json-stable-stringify": "2.x"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
         "node_modules/bser": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@@ -4249,6 +4282,22 @@
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/ejs": {
+            "version": "3.1.10",
+            "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+            "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "dependencies": {
+                "jake": "^10.8.5"
+            },
+            "bin": {
+                "ejs": "bin/cli.js"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/electron-to-chromium": {
             "version": "1.5.19",
             "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz",
@@ -5098,6 +5147,39 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/filelist": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
+            "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "dependencies": {
+                "minimatch": "^5.0.1"
+            }
+        },
+        "node_modules/filelist/node_modules/brace-expansion": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+            "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "balanced-match": "^1.0.0"
+            }
+        },
+        "node_modules/filelist/node_modules/minimatch": {
+            "version": "5.1.6",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+            "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
         "node_modules/fill-range": {
             "version": "7.1.1",
             "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -5971,6 +6053,101 @@
                 "@pkgjs/parseargs": "^0.11.0"
             }
         },
+        "node_modules/jake": {
+            "version": "10.9.2",
+            "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
+            "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "dependencies": {
+                "async": "^3.2.3",
+                "chalk": "^4.0.2",
+                "filelist": "^1.0.4",
+                "minimatch": "^3.1.2"
+            },
+            "bin": {
+                "jake": "bin/cli.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/jake/node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/jake/node_modules/chalk": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/jake/node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/jake/node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/jake/node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/jake/node_modules/supports-color": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/jest": {
             "version": "29.7.0",
             "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
@@ -8102,6 +8279,13 @@
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/lodash.memoize": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+            "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/lodash.sortby": {
             "version": "4.7.0",
             "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -8155,6 +8339,13 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/make-error": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+            "dev": true,
+            "license": "ISC"
+        },
         "node_modules/makeerror": {
             "version": "1.0.12",
             "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -10874,6 +11065,92 @@
             "dev": true,
             "license": "Apache-2.0"
         },
+        "node_modules/ts-jest": {
+            "version": "29.3.4",
+            "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz",
+            "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "bs-logger": "^0.2.6",
+                "ejs": "^3.1.10",
+                "fast-json-stable-stringify": "^2.1.0",
+                "jest-util": "^29.0.0",
+                "json5": "^2.2.3",
+                "lodash.memoize": "^4.1.2",
+                "make-error": "^1.3.6",
+                "semver": "^7.7.2",
+                "type-fest": "^4.41.0",
+                "yargs-parser": "^21.1.1"
+            },
+            "bin": {
+                "ts-jest": "cli.js"
+            },
+            "engines": {
+                "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0"
+            },
+            "peerDependencies": {
+                "@babel/core": ">=7.0.0-beta.0 <8",
+                "@jest/transform": "^29.0.0",
+                "@jest/types": "^29.0.0",
+                "babel-jest": "^29.0.0",
+                "jest": "^29.0.0",
+                "typescript": ">=4.3 <6"
+            },
+            "peerDependenciesMeta": {
+                "@babel/core": {
+                    "optional": true
+                },
+                "@jest/transform": {
+                    "optional": true
+                },
+                "@jest/types": {
+                    "optional": true
+                },
+                "babel-jest": {
+                    "optional": true
+                },
+                "esbuild": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/ts-jest/node_modules/semver": {
+            "version": "7.7.2",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+            "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+            "dev": true,
+            "license": "ISC",
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/ts-jest/node_modules/type-fest": {
+            "version": "4.41.0",
+            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+            "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+            "dev": true,
+            "license": "(MIT OR CC0-1.0)",
+            "engines": {
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/ts-jest/node_modules/yargs-parser": {
+            "version": "21.1.1",
+            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+            "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+            "dev": true,
+            "license": "ISC",
+            "engines": {
+                "node": ">=12"
+            }
+        },
         "node_modules/tslib": {
             "version": "2.7.0",
             "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
diff --git a/package.json b/package.json
index 7c7c49f..d379838 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,8 @@
     "scripts": {
         "dev": "node ./esbuild.config.mjs",
         "build": "node ./esbuild.config.mjs production",
-        "push": "git push --follow-tags origin"
+        "push": "git push --follow-tags origin",
+        "test": "jest"
     },
     "types": "./dist/api.d.ts",
     "keywords": [],
@@ -27,6 +28,7 @@
         "@popperjs/core": "^2.11.8",
         "@tsconfig/svelte": "^5.0.4",
         "@types/dom-to-image": "^2.6.7",
+        "@types/jest": "^29.5.14",
         "@types/node": "^22.5.4",
         "@types/xmldom": "^0.1.34",
         "@xmldom/xmldom": "^0.9.2",
@@ -53,6 +55,7 @@
         "svelte-preprocess": "^6.0.2",
         "svelte-select": "^5.8.3",
         "title-case": "^4.3.1",
+        "ts-jest": "^29.3.4",
         "tslib": "^2.7.0",
         "tsup": "^8.2.4",
         "typescript": "^5.6.2"
diff --git a/src/importers/importer.worker.ts b/src/importers/importer.worker.ts
index 6a7fe6c..2899fab 100644
--- a/src/importers/importer.worker.ts
+++ b/src/importers/importer.worker.ts
@@ -8,6 +8,7 @@ import {
 import { build5eMonsterFromFile } from "./5eToolsImport";
 import { buildMonsterFromTetraCube } from "./TetraCubeImport";
 import { buildMonsterFromPathbuilderFile } from "./PathbuilderImport";
+import { buildMonsterFromPf2eToolsFile } from "./pf2eToolsImport";
 
 const ctx: Worker = self as any;
 
@@ -55,6 +56,11 @@ ctx.onmessage = async (event) => {
                 monsters.push(...(imported ?? []));
                 break;
             }
+            case "pf2etools": {
+                const imported = await buildMonsterFromPf2eToolsFile(file);
+                monsters.push(...(imported ?? []));
+                break;
+            }
             case "generic": {
                 const imported: Monster[] = await new Promise(
                     (resolve, reject) => {
diff --git a/src/importers/index.ts b/src/importers/index.ts
index 32a4ed6..48f3bf4 100644
--- a/src/importers/index.ts
+++ b/src/importers/index.ts
@@ -3,5 +3,6 @@ export * from "./CritterDBImport";
 export * from "./DnDAppFilesImport";
 export * from "./ImprovedInitiativeImport";
 export * from "./pf2eMonsterToolImport";
+export * from "./pf2eToolsImport";
 export * from "./PathbuilderImport";
 export * from "./Pathbuilder.d";
diff --git a/src/importers/pf2eMonsterToolImport.ts b/src/importers/pf2eMonsterToolImport.ts
index 52e13a6..8d52624 100644
--- a/src/importers/pf2eMonsterToolImport.ts
+++ b/src/importers/pf2eMonsterToolImport.ts
@@ -1,4 +1,4 @@
-import { Monster, Trait } from "index";
+import { Monster, Trait } from "../../index";
 
 
 export async function buildMonsterFromPF2EMonsterToolFile(
diff --git a/src/importers/pf2eToolsImport.ts b/src/importers/pf2eToolsImport.ts
new file mode 100644
index 0000000..ce6931f
--- /dev/null
+++ b/src/importers/pf2eToolsImport.ts
@@ -0,0 +1,435 @@
+import { Monster, Trait } from "../../index";
+import { ONE_ACTION, addSign, getACStats, getModifierToDiceRoll, toTitleCase } from "./pf2eMonsterToolImport";
+
+export interface Pf2eToolsCreature {
+    name: string;
+    source: string;
+    page?: number;
+    level: number;
+    traits: string[];
+    perception: {
+        std: number;
+    };
+    senses?: {
+        name: string;
+    }[];
+    languages?: {
+        languages?: string[];
+        abilities?: string[];
+    };
+    skills?: Record<string, {
+        std: number;
+        note?: string;
+    }>;
+    abilityMods: {
+        str: number;
+        dex: number;
+        con: number;
+        int: number;
+        wis: number;
+        cha: number;
+    };
+    items?: string[];
+    speed: {
+        walk?: number;
+        fly?: number;
+        swim?: number;
+        climb?: number;
+        burrow?: number;
+    };
+    attacks?: {
+        range: "Melee" | "Ranged";
+        name: string;
+        attack: number;
+        traits?: string[];
+        damage?: string;
+        types?: string[];
+    }[];
+    spellcasting?: {
+        type: string;
+        tradition?: string;
+        DC?: number;
+        fp?: number;
+        name?: string;
+        entry: Record<string, {
+            level?: number;
+            spells: {
+                name: string;
+            }[];
+        }>;
+    }[];
+    abilities?: {
+        top?: Ability[];
+        mid?: Ability[];
+        bot?: Ability[];
+    };
+    defenses: {
+        ac: {
+            std: number;
+            [key: string]: number;
+        };
+        savingThrows: {
+            fort: {
+                std: number;
+            };
+            ref: {
+                std: number;
+            };
+            will: {
+                std: number;
+            };
+        };
+        hp: {
+            hp: number;
+            note?: string;
+        }[];
+        immunities?: string[];
+        resistances?: {
+            amount: number;
+            name: string;
+            note?: string;
+        }[];
+        weaknesses?: {
+            amount: number;
+            name: string;
+            note?: string;
+        }[];
+    };
+}
+
+interface Ability {
+    name: string;
+    entries?: (string | { type: string; items: string[] })[];
+    activity?: {
+        number: number;
+        unit: string;
+    };
+    trigger?: string;
+}
+
+export async function buildMonsterFromPf2eToolsFile(
+    file: File
+): Promise<Monster[]> {
+    return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.onload = async (event: any) => {
+            try {
+                let json = JSON.parse(event.target.result);
+                const monsters: Monster[] = [];
+
+                // The Pf2eTools format has creatures in a "creature" array
+                if (json.creature && Array.isArray(json.creature)) {
+                    for (const creatureData of json.creature) {
+                        try {
+                            monsters.push(convertPf2eToolsCreatureToMonster(creatureData));
+                        } catch (error) {
+                            console.error(`Error converting creature ${creatureData.name || 'unknown'}:`, error);
+                            // Skip this monster and continue with the next one
+                        }
+                    }
+                }
+
+                resolve(monsters);
+            } catch (e) {
+                console.error(`Error parsing Pf2eTools file:`, e);
+                reject(e);
+            }
+        };
+        reader.readAsText(file);
+    });
+}
+
+export function convertPf2eToolsCreatureToMonster(creature: Pf2eToolsCreature): Monster {
+    const stats: [number, number, number, number, number, number] = [
+        creature.abilityMods.str,
+        creature.abilityMods.dex,
+        creature.abilityMods.con,
+        creature.abilityMods.int,
+        creature.abilityMods.wis,
+        creature.abilityMods.cha
+    ];
+    
+    const ac = creature.defenses.ac.std;
+    const fortitude = creature.defenses.savingThrows.fort.std;
+    const reflex = creature.defenses.savingThrows.ref.std;
+    const will = creature.defenses.savingThrows.will.std;
+    
+    // Extract the HP value
+    const hp = creature.defenses.hp[0].hp;
+    const hpNote = creature.defenses.hp[0].note || "";
+    
+    // Create speed string
+    const speedEntries: string[] = [];
+    if (creature.speed.walk) speedEntries.push(`${creature.speed.walk} feet`);
+    if (creature.speed.fly) speedEntries.push(`fly ${creature.speed.fly} feet`);
+    if (creature.speed.swim) speedEntries.push(`swim ${creature.speed.swim} feet`);
+    if (creature.speed.climb) speedEntries.push(`climb ${creature.speed.climb} feet`);
+    if (creature.speed.burrow) speedEntries.push(`burrow ${creature.speed.burrow} feet`);
+    
+    const speedString = speedEntries.join(", ");
+    
+    // Process traits
+    const traits = creature.traits || [];
+    
+    // Process abilities
+    const abilities_top = processAbilities(creature.abilities?.top || []);
+    const abilities_mid = processAbilities(creature.abilities?.mid || []);
+    const abilities_bot = processAbilities(creature.abilities?.bot || []);
+    
+    // Process attacks
+    const attacks = processAttacks(creature.attacks || []);
+    
+    // Process spellcasting
+    const spellcasting = processSpellcasting(creature.spellcasting || []);
+    
+    // Process senses
+    const sensesArray = creature.senses?.map(s => s.name) || [];
+    const senses = sensesArray.join(", ");
+    
+    // Process languages
+    let languagesList: string[] = [];
+    if (creature.languages?.languages) {
+        languagesList = languagesList.concat(creature.languages.languages);
+    }
+    if (creature.languages?.abilities) {
+        languagesList = languagesList.concat(creature.languages.abilities);
+    }
+    const languages = languagesList.join(", ");
+    
+    // Process skills
+    const skills = processSkills(creature.skills || {});
+    
+    // Process immunities, resistances, and weaknesses
+    const immunities = (creature.defenses.immunities || []).join(", ");
+    
+    const resistances = (creature.defenses.resistances || [])
+        .map(r => `${r.name} ${r.amount}${r.note ? ` (${r.note})` : ""}`)
+        .join(", ");
+    
+    const weaknesses = (creature.defenses.weaknesses || [])
+        .map(w => `${w.name} ${w.amount}${w.note ? ` (${w.note})` : ""}`)
+        .join(", ");
+    
+    // Create the monster object
+    const monster: Monster = {
+        layout: "Basic Pathfinder 2e Layout",
+        name: creature.name,
+        level: `Creature ${creature.level}`,
+        size: getSizeString(traits),
+        trait_03: getTypeFromTraits(traits),
+        modifier: creature.perception.std,
+        perception: [{
+            name: "Perception",
+            desc: `Perception ${addSign(creature.perception.std)};${senses ? ` ${senses}` : ""}`
+        }],
+        abilities_top,
+        abilities_mid,
+        abilities_bot,
+        type: getTypeFromTraits(traits),
+        subtype: "",
+        alignment: getAlignmentFromTraits(traits),
+        ac,
+        armorclass: getACStats(ac, fortitude, reflex, will),
+        hp,
+        health: [{
+            name: "HP",
+            desc: `${hp}${hpNote ? ` (${hpNote})` : ""}${immunities ? `; __Immunities__ ${immunities}` : ""}${resistances ? `; __Resistances__ ${resistances}` : ""}${weaknesses ? `; __Weaknesses__ ${weaknesses}` : ""}`
+        }],
+        attacks,
+        spellcasting,
+        speed: speedString,
+        stats,
+        abilityMods: stats,
+        damage_vulnerabilities: weaknesses,
+        damage_resistances: resistances,
+        damage_immunities: immunities,
+        condition_immunities: "",
+        senses,
+        languages,
+        cr: creature.level,
+        bestiary: false,
+        skills,
+        source: creature.source
+    };
+    
+    // Add traits
+    for (let i = 0; i < traits.length; i++) {
+        const traitIndexStr = (i + 4).toString().padStart(2, '0');
+        const traitKeyString = `trait_${traitIndexStr}`;
+        monster[traitKeyString] = traits[i];
+    }
+    
+    return monster;
+}
+
+function processAbilities(abilities: Ability[]): Trait[] {
+    return abilities.map(ability => {
+        const actionIcon = getActionIcon(ability);
+        const description = processEntries(ability.entries || []);
+        
+        let desc = "";
+        if (actionIcon) desc += actionIcon + " ";
+        if (ability.trigger) desc += `__Trigger__ ${ability.trigger} `;
+        desc += description;
+        
+        return {
+            name: ability.name,
+            desc
+        };
+    });
+}
+
+function processEntries(entries: (string | { type: string; items: string[] })[]): string {
+    let result = "";
+    
+    for (const entry of entries) {
+        if (typeof entry === "string") {
+            // Replace Pf2eTools-specific formatting
+            let processed = entry.replace(/{@condition ([^}]*)}/g, "__$1__")
+                               .replace(/{@action ([^}]*)}/g, "__$1__")
+                               .replace(/{@trait ([^}]*)}/g, "__$1__")
+                               .replace(/{@damage ([^}]*)}/g, "$1")
+                               .replace(/{@item ([^}]*)}/g, "$1");
+            
+            result += processed + " ";
+        } else if (entry.type === "list") {
+            result += entry.items.map(item => `• ${item}`).join("\n");
+        }
+    }
+    
+    return result.trim();
+}
+
+function processAttacks(attacks: Pf2eToolsCreature["attacks"]): Trait[] {
+    return attacks.map(attack => {
+        const traits = attack.traits ? ` (${attack.traits.join(", ")})` : "";
+        const damage = attack.damage ? ` __Damage__ ${attack.damage.replace(/{@damage ([^}]*)}/g, "$1")}` : "";
+        
+        return {
+            name: attack.range,
+            desc: ONE_ACTION + ` ${attack.name} ${addSign(attack.attack)}${traits}${damage}`
+        };
+    });
+}
+
+function processSpellcasting(spellcasting: Pf2eToolsCreature["spellcasting"]): Trait[] {
+    return spellcasting.map(casting => {
+        const name = casting.name || casting.type;
+        let description = "";
+        
+        if (casting.tradition) description += `${casting.tradition} `;
+        if (casting.DC) description += `DC ${casting.DC}; `;
+        if (casting.fp) description += `${casting.fp} Focus Points; `;
+        
+        // Process spells by level
+        const spellLevels = Object.keys(casting.entry);
+        for (const levelKey of spellLevels) {
+            const levelData = casting.entry[levelKey];
+            const level = levelData.level || parseInt(levelKey);
+            const spellNames = levelData.spells.map(s => s.name).join(", ");
+            
+            if (levelKey === "0") {
+                description += `__Cantrips (${level})__ ${spellNames}; `;
+            } else {
+                description += `__Level ${levelKey}__ ${spellNames}; `;
+            }
+        }
+        
+        return {
+            name,
+            desc: description.trim()
+        };
+    });
+}
+
+function processSkills(skills: Record<string, { std: number; note?: string }>): Trait[] {
+    const skillEntries = Object.entries(skills).map(([name, data]) => {
+        const skillName = toTitleCase(name);
+        const value = data.std;
+        const note = data.note ? ` (${data.note})` : "";
+        
+        return `__${skillName}__: ${getModifierToDiceRoll(value)}${note}`;
+    });
+    
+    if (skillEntries.length === 0) {
+        return [];
+    }
+    
+    return [{
+        name: "Skills",
+        desc: skillEntries.join(" ")
+    }];
+}
+
+function getSizeString(traits: string[]): string {
+    const sizeMap: Record<string, string> = {
+        "tiny": "Tiny",
+        "small": "Small",
+        "medium": "Medium",
+        "large": "Large",
+        "huge": "Huge",
+        "gargantuan": "Gargantuan"
+    };
+    
+    for (const trait of traits) {
+        const size = sizeMap[trait.toLowerCase()];
+        if (size) return size;
+    }
+    
+    return "Medium"; // Default
+}
+
+function getTypeFromTraits(traits: string[]): string {
+    const typeOrder = [
+        "aberration", "animal", "astral", "beast", "celestial", "construct", 
+        "dragon", "elemental", "fey", "fiend", "giant", "humanoid", 
+        "monitor", "ooze", "plant", "spirit", "undead"
+    ];
+    
+    for (const type of typeOrder) {
+        if (traits.some(t => t.toLowerCase() === type)) {
+            return toTitleCase(type);
+        }
+    }
+    
+    return "Humanoid"; // Default
+}
+
+function getAlignmentFromTraits(traits: string[]): string {
+    const alignmentMap: Record<string, string> = {
+        "lg": "Lawful Good",
+        "ng": "Neutral Good",
+        "cg": "Chaotic Good",
+        "ln": "Lawful Neutral",
+        "n": "Neutral",
+        "cn": "Chaotic Neutral",
+        "le": "Lawful Evil",
+        "ne": "Neutral Evil",
+        "ce": "Chaotic Evil"
+    };
+    
+    for (const trait of traits) {
+        const alignment = alignmentMap[trait.toLowerCase()];
+        if (alignment) return alignment;
+    }
+    
+    return "Neutral"; // Default
+}
+
+function getActionIcon(ability: Ability): string {
+    if (!ability.activity) return "";
+    
+    const { number, unit } = ability.activity;
+    
+    if (unit === "action") {
+        if (number === 1) return ONE_ACTION;
+        if (number === 2) return "⬺ ";
+        if (number === 3) return "⬽ ";
+    } else if (unit === "reaction") {
+        return "⬲ ";
+    } else if (unit === "free") {
+        return "⭓ ";
+    }
+    
+    return "";
+}
\ No newline at end of file
diff --git a/src/settings/settings.ts b/src/settings/settings.ts
index bf91b28..fddad27 100644
--- a/src/settings/settings.ts
+++ b/src/settings/settings.ts
@@ -988,6 +988,34 @@ export default class StatblockSettingTab extends PluginSettingTab {
             b.onClick(() => inputPathbuilder.click());
         });
 
+        const importPf2eTools = new Setting(importAdditional)
+            .setName("Import Pf2eTools Data")
+            .setDesc("Only import content that you own.");
+        const inputPf2eTools = createEl("input", {
+            attr: {
+                type: "file",
+                name: "pf2etools",
+                accept: ".json",
+                multiple: true
+            }
+        });
+        inputPf2eTools.onchange = async () => {
+            const { files } = inputPf2eTools;
+            if (!files?.length) return;
+            const monsters = await this.importer.import(files, "pf2etools");
+            if (monsters && monsters.length) {
+                await this.plugin.saveMonsters(monsters);
+            }
+            this.display();
+        };
+        importPf2eTools.addButton((b) => {
+            b.setButtonText("Choose File(s)").setTooltip(
+                "Import Pf2eTools Data"
+            );
+            b.buttonEl.addClass("statblock-file-upload");
+            b.buttonEl.appendChild(inputPf2eTools);
+            b.onClick(() => inputPf2eTools.click());
+        });
 
 
         const importGeneric = new Setting(importAdditional)
diff --git a/test/importers/pf2eToolsImport.test.ts b/test/importers/pf2eToolsImport.test.ts
new file mode 100644
index 0000000..0eecc28
--- /dev/null
+++ b/test/importers/pf2eToolsImport.test.ts
@@ -0,0 +1,205 @@
+import { buildMonsterFromPf2eToolsFile, convertPf2eToolsCreatureToMonster, Pf2eToolsCreature } from '../../src/importers/pf2eToolsImport';
+import { Monster } from '../../index';
+import * as fs from 'fs';
+import * as path from 'path';
+
+// Helper to access private function for testing
+// This is a workaround since convertPf2eToolsCreatureToMonster is not exported in the original file
+declare module '../../src/importers/pf2eToolsImport' {
+    export function convertPf2eToolsCreatureToMonster(creature: Pf2eToolsCreature): any;
+}
+
+describe('convertPf2eToolsCreatureToMonster', () => {
+    // Load test data from the creatures-afof.json file
+    const testDataPath = path.resolve(__dirname, '../../tmp/creatures-afof.json');
+    const jsonData = JSON.parse(fs.readFileSync(testDataPath, 'utf-8'));
+    
+    // Test with the "Darius" creature from the file
+    const dariusData = jsonData.creature.find((c: any) => c.name === 'Darius');
+    
+    test('should convert Darius creature correctly', () => {
+        // Convert the creature
+        const monster = convertPf2eToolsCreatureToMonster(dariusData);
+        
+        // Basic assertions for the conversion
+        expect(monster).toBeDefined();
+        expect(monster.name).toBe('Darius');
+        expect(monster.level).toBe('Creature 3');
+        expect(monster.size).toBe('Medium');
+        
+        // Check ability modifiers
+        expect(monster.stats).toEqual([3, 1, 2, 3, 0, 1]);
+        
+        // Check defenses
+        expect(monster.ac).toBe(18);
+        expect(monster.hp).toBe(45);
+        
+        // Check attacks
+        expect(monster.attacks).toHaveLength(3);
+        expect(monster.attacks[0].name).toBe('Melee');
+        expect(monster.attacks[0].desc).toContain('jaws +12');
+        
+        // Check abilities
+        expect(monster.abilities_mid).toHaveLength(1);
+        expect(monster.abilities_mid[0].name).toBe('Metabolize Mutagen');
+        
+        expect(monster.abilities_bot).toHaveLength(2);
+        expect(monster.abilities_bot[0].name).toBe('Crystalline Mutation');
+        expect(monster.abilities_bot[1].name).toBe('Spray Hot Wax');
+    });
+
+    // Test with the "Scented Candle Homunculus" creature from the file
+    const homunculus = jsonData.creature.find((c: any) => c.name === 'Scented Candle Homunculus');
+    
+    test('should convert Scented Candle Homunculus correctly', () => {
+        // Protect against undefined data
+        if (!homunculus) {
+            console.warn('Test data not found for Scented Candle Homunculus');
+            return;
+        }
+
+        // Convert the creature
+        const monster = convertPf2eToolsCreatureToMonster(homunculus);
+        
+        // Basic assertions for the conversion
+        expect(monster).toBeDefined();
+        expect(monster.name).toBe('Scented Candle Homunculus');
+        expect(monster.level).toBe('Creature 1');
+        expect(monster.size).toBe('Tiny');
+        expect(monster.type).toBe('Construct');
+        
+        // Check senses
+        expect(monster.senses).toContain('darkvision');
+    });
+
+    // Edge case: Test with minimal required creature data
+    test('should handle minimal creature data', () => {
+        const minimalCreature: Pf2eToolsCreature = {
+            name: 'Minimal Creature',
+            source: 'Test',
+            level: 1,
+            traits: ['medium', 'humanoid'],
+            perception: {
+                std: 0
+            },
+            abilityMods: {
+                str: 0,
+                dex: 0,
+                con: 0,
+                int: 0,
+                wis: 0,
+                cha: 0
+            },
+            speed: {},
+            defenses: {
+                ac: {
+                    std: 10
+                },
+                savingThrows: {
+                    fort: { std: 0 },
+                    ref: { std: 0 },
+                    will: { std: 0 }
+                },
+                hp: [{ hp: 10 }]
+            }
+        };
+        
+        const monster = convertPf2eToolsCreatureToMonster(minimalCreature);
+        
+        expect(monster).toBeDefined();
+        expect(monster.name).toBe('Minimal Creature');
+        expect(monster.level).toBe('Creature 1');
+        expect(monster.hp).toBe(10);
+        expect(monster.ac).toBe(10);
+    });
+    
+    // Test error handling: Test with invalid creature data that will cause an error
+    test('should throw an error with invalid creature data', () => {
+        const invalidCreature: any = {
+            name: 'Invalid Creature',
+            source: 'Test',
+            level: 1,
+            traits: ['medium', 'humanoid'],
+            perception: {
+                std: 0
+            },
+            abilityMods: {
+                // Missing required ability scores
+                str: 0,
+                dex: 0
+                // con, int, wis, cha are missing
+            },
+            speed: {},
+            // Missing defenses
+        };
+        
+        expect(() => {
+            convertPf2eToolsCreatureToMonster(invalidCreature);
+        }).toThrow();
+    });
+});
+
+// Test the error handling in the creature conversion loop
+describe('Error handling in creature conversion', () => {
+    test('should skip invalid creatures and process valid ones', () => {
+        // Mock console.error to capture calls
+        const originalConsoleError = console.error;
+        const mockConsoleError = jest.fn();
+        console.error = mockConsoleError;
+        
+        const monsters: any[] = [];
+        
+        // Mixed array of valid and invalid creatures
+        const creatures = [
+            // Valid creature - use correct typing
+            {
+                name: 'Valid Creature',
+                source: 'Test',
+                level: 1,
+                traits: ['medium', 'humanoid'],
+                perception: { std: 0 },
+                abilityMods: {
+                    str: 0, dex: 0, con: 0, int: 0, wis: 0, cha: 0
+                },
+                speed: {},
+                defenses: {
+                    ac: { std: 10 },
+                    savingThrows: {
+                        fort: { std: 0 },
+                        ref: { std: 0 },
+                        will: { std: 0 }
+                    },
+                    hp: [{ hp: 10 }]
+                }
+            } as Pf2eToolsCreature,
+            // Invalid creature - use 'as any' to bypass type checking
+            {
+                name: 'Invalid Creature',
+                source: 'Test',
+                level: 1,
+                // Missing many required fields
+            } as any
+        ];
+        
+        // Simulate the conversion loop from buildMonsterFromPf2eToolsFile
+        for (const creatureData of creatures) {
+            try {
+                monsters.push(convertPf2eToolsCreatureToMonster(creatureData));
+            } catch (error) {
+                console.error(`Error converting creature ${creatureData.name || 'unknown'}:`, error);
+                // Skip this monster and continue with the next one
+            }
+        }
+        
+        // Restore console.error
+        console.error = originalConsoleError;
+        
+        // Verify that one monster was successfully converted
+        expect(monsters.length).toBe(1);
+        expect(monsters[0].name).toBe('Valid Creature');
+        
+        // Verify that console.error was called for the invalid creature
+        expect(mockConsoleError).toHaveBeenCalled();
+        expect(mockConsoleError.mock.calls[0][0]).toContain('Error converting creature Invalid Creature');
+    });
+});
\ No newline at end of file
diff --git a/tmp/creatures-afof.json b/tmp/creatures-afof.json
new file mode 100644
index 0000000..fa80695
--- /dev/null
+++ b/tmp/creatures-afof.json
@@ -0,0 +1,954 @@
+{
+	"_meta": {
+		"dependencies": {
+			"creature": [
+				"B1"
+			]
+		}
+	},
+	"creature": [
+		{
+			"name": "Glimmer",
+			"source": "AFoF",
+			"page": 4,
+			"description": "Female {@creature pixie|b1}",
+			"_copy": {
+				"name": "Pixie",
+				"source": "B1"
+			}
+		},
+		{
+			"name": "Darius",
+			"source": "AFoF",
+			"page": 8,
+			"level": 3,
+			"traits": [
+				"unique",
+				"n",
+				"medium",
+				"human",
+				"humanoid"
+			],
+			"description": "Male {@ancestry human} alchemist",
+			"perception": {
+				"std": 7
+			},
+			"languages": {
+				"languages": [
+					"common"
+				]
+			},
+			"skills": {
+				"athletics": {
+					"std": 10
+				},
+				"crafting": {
+					"std": 10
+				},
+				"deception": {
+					"std": 8
+				},
+				"occultism": {
+					"std": 8
+				},
+				"society": {
+					"std": 10
+				}
+			},
+			"abilityMods": {
+				"str": 3,
+				"dex": 1,
+				"con": 2,
+				"int": 3,
+				"wis": 0,
+				"cha": 1
+			},
+			"items": [
+				"key to basement",
+				"singed leather apron and clothing",
+				"{@item moderate bestial mutagen||moderate bestial mutagens} (2)",
+				"improvised wax sprayer",
+				"wax stirring stick"
+			],
+			"defenses": {
+				"ac": {
+					"std": 18
+				},
+				"savingThrows": {
+					"fort": {
+						"std": 11
+					},
+					"ref": {
+						"std": 8
+					},
+					"will": {
+						"std": 5
+					}
+				},
+				"hp": [
+					{
+						"hp": 45
+					}
+				]
+			},
+			"speed": {
+				"walk": 25
+			},
+			"attacks": [
+				{
+					"range": "Melee",
+					"traits": [
+						"agile",
+						"unarmed"
+					],
+					"name": "jaws",
+					"attack": 12,
+					"damage": "{@damage 1d8+5} piercing"
+				},
+				{
+					"range": "Melee",
+					"traits": [
+						"unarmed"
+					],
+					"name": "claw",
+					"attack": 12,
+					"damage": "{@damage 1d10+5} slashing"
+				},
+				{
+					"range": "Melee",
+					"traits": [
+						"agile",
+						"nonlethal",
+						"unarmed"
+					],
+					"name": "wax stirring stick",
+					"attack": 10,
+					"damage": "{@damage 1d6+5} bludgeoning"
+				}
+			],
+			"abilities": {
+				"mid": [
+					{
+						"name": "Metabolize Mutagen",
+						"activity": {
+							"number": 1,
+							"unit": "action"
+						},
+						"traits": [
+							"concentrate",
+							"manipulate"
+						],
+						"entries": [
+							"Darius metabolizes the power of an active mutagen to regain {@dice 2d6} Hit Points. If he does so (or if he's confronted by a stealthy party before he's had a chance to drink a mutagen), he loses his jaws and claw unarmed attacks, his bonus on {@skill Athletics} checks decreases by 2, his AC increases by 1, and his Reflex save bonus increases by 2."
+						]
+					}
+				],
+				"bot": [
+					{
+						"name": "Crystalline Mutation",
+						"entries": [
+							"When Darius drinks any mutagen, his teeth and fingernails grow sharp crystalline edges."
+						]
+					},
+					{
+						"name": "Spray Hot Wax",
+						"activity": {
+							"number": 2,
+							"unit": "action"
+						},
+						"entries": [
+							"Darius sprays hot wax from his improvised wax sprayer in a 15-foot cone. All creatures in the area must attempt a {@dc 20} Reflex save. Scented candle homunculi in the area take no damage, instead healing {@dice 2d6} damage. After using the hot wax cannon, Darius can't use it again until he takes two {@action Interact} actions while adjacent to the cauldron of bubbling wax to reload the sprayer.",
+							{
+								"type": "successDegree",
+								"entries": {
+									"Critical Success": [
+										"The creature takes no damage."
+									],
+									"Success": [
+										"The creature takes {@damage 2d6} fire damage."
+									],
+									"Failure": [
+										"The creature takes {@damage 3d6} fire damage plus {@damage 1d6} {@condition persistent damage||persistent fire damage}."
+									],
+									"Critical Failure": [
+										"The creature takes {@damage 5d6} fire damage plus {@damage 2d6} {@condition persistent damage||persistent fire damage}."
+									]
+								}
+							}
+						]
+					}
+				]
+			}
+		},
+		{
+			"name": "Scented Candle Homunculus",
+			"source": "AFoF",
+			"page": 9,
+			"level": 1,
+			"traits": [
+				"rare",
+				"ne",
+				"tiny",
+				"construct"
+			],
+			"description": "Variant {@creature homunculus}",
+			"perception": {
+				"std": 3
+			},
+			"senses": [
+				{
+					"name": "darkvision"
+				}
+			],
+			"languages": {
+				"languages": [
+					"common (can't speak any language)"
+				],
+				"abilities": [
+					"master link"
+				]
+			},
+			"skills": {
+				"acrobatics": {
+					"std": 5
+				},
+				"stealth": {
+					"std": 5
+				}
+			},
+			"abilityMods": {
+				"str": -1,
+				"dex": 3,
+				"con": 0,
+				"int": 1,
+				"wis": 1,
+				"cha": -2
+			},
+			"defenses": {
+				"ac": {
+					"std": 17
+				},
+				"savingThrows": {
+					"fort": {
+						"std": 2
+					},
+					"ref": {
+						"std": 7
+					},
+					"will": {
+						"std": 3
+					}
+				},
+				"hp": [
+					{
+						"hp": 17
+					}
+				],
+				"immunities": [
+					"bleed",
+					"death effects",
+					"disease",
+					"doomed",
+					"drained",
+					"fatigued",
+					"healing",
+					"necromancy",
+					"nonlethal attacks",
+					"paralyzed",
+					"poison",
+					"sickened",
+					"unconscious"
+				]
+			},
+			"speed": {
+				"walk": 15,
+				"fly": 40
+			},
+			"attacks": [
+				{
+					"range": "Melee",
+					"traits": [
+						"finesse",
+						"magical"
+					],
+					"name": "jaws",
+					"attack": 7,
+					"damage": "{@damage 1d4} piercing plus {@damage 1d4} {@condition persistent damage||persistent fire}"
+				},
+				{
+					"range": "Ranged",
+					"traits": [
+						"magical",
+						"range increment <20 feet>"
+					],
+					"name": "wax glob",
+					"attack": 8,
+					"damage": "{@damage 1d4} {@condition persistent damage||persistent fire}"
+				}
+			],
+			"abilities": {
+				"top": [
+					{
+						"name": "Master Link",
+						"traits": [
+							"arcane",
+							"divination",
+							"mental"
+						],
+						"entries": [
+							"As homunculus."
+						]
+					}
+				],
+				"mid": [
+					{
+						"name": "Perfume Aura",
+						"traits": [
+							"aura",
+							"olfactory",
+							"poison"
+						],
+						"entries": [
+							"5 feet. The scented candle homunculus's fragrance of lavender is overwhelmingly intense. Any creature other than the homunculus's master who enters the aura must attempt a {@dc 15} Fortitude save.",
+							{
+								"type": "successDegree",
+								"entries": {
+									"Critical Success": [
+										"The creature is temporarily immune to the homunculus's aura for 24 hours."
+									],
+									"Success": [
+										"The creature is unaffected."
+									],
+									"Failure": [
+										"The creature is {@condition sickened||sickened 1}."
+									],
+									"Critical Failure": [
+										"The creature is {@condition stupefied||stupefied 1} for 1 round and {@condition sickened||sickened 1}."
+									]
+								}
+							}
+						]
+					}
+				],
+				"bot": [
+					{
+						"name": "Dunk in Wax",
+						"activity": {
+							"number": 2,
+							"unit": "action"
+						},
+						"traits": [
+							"move"
+						],
+						"entries": [
+							"A scented candle homunculus who is adjacent to the large wax cauldron can dunk itself in wax, healing {@dice 2d6} damage."
+						]
+					}
+				]
+			}
+		},
+		{
+			"name": "Golden Bamboo Leshy",
+			"source": "AFoF",
+			"page": 11,
+			"level": 2,
+			"traits": [
+				"unique",
+				"n",
+				"small",
+				"leshy",
+				"plant"
+			],
+			"perception": {
+				"std": 8
+			},
+			"senses": [
+				{
+					"name": "low-light vision"
+				}
+			],
+			"languages": {
+				"languages": [
+					"common",
+					"druidic",
+					"sylvan"
+				],
+				"abilities": [
+					"{@spell speak with plants} (same type as leshy)"
+				]
+			},
+			"skills": {
+				"athletics": {
+					"std": 6
+				},
+				"nature": {
+					"std": 6
+				},
+				"stealth": {
+					"std": 8
+				}
+			},
+			"abilityMods": {
+				"str": 2,
+				"dex": 4,
+				"con": 2,
+				"int": -1,
+				"wis": 2,
+				"cha": 0
+			},
+			"defenses": {
+				"ac": {
+					"std": 18
+				},
+				"savingThrows": {
+					"fort": {
+						"std": 8
+					},
+					"ref": {
+						"std": 10
+					},
+					"will": {
+						"std": 6
+					}
+				},
+				"hp": [
+					{
+						"hp": 30
+					}
+				]
+			},
+			"speed": {
+				"walk": 25
+			},
+			"attacks": [
+				{
+					"range": "Melee",
+					"traits": [
+						"agile",
+						"finesse"
+					],
+					"name": "fist",
+					"attack": 10,
+					"damage": "{@damage 1d6+4} bludgeoning"
+				}
+			],
+			"spellcasting": [
+				{
+					"tradition": "primal",
+					"type": "Innate",
+					"DC": 16,
+					"entry": {
+						"4": {
+							"spells": [
+								{
+									"name": "speak with plants"
+								}
+							]
+						}
+					}
+				}
+			],
+			"abilities": {
+				"mid": [
+					{
+						"name": "Verdant Burst",
+						"traits": [
+							"healing"
+						],
+						"entries": [
+							"When a leshy dies, a burst of primal energy explodes from its body, restoring {@dice 2d8} Hit Points to each plant creature in a 30-foot emanation. This area is filled with plants of the same type as the leshy, becoming difficult terrain. If the terrain isn't a viable environment for these plants, they wither after 24 hours."
+						]
+					}
+				],
+				"bot": [
+					{
+						"name": "Change Shape",
+						"activity": {
+							"number": 1,
+							"unit": "action"
+						},
+						"traits": [
+							"concentrate",
+							"polymorph",
+							"primal",
+							"transmutation"
+						],
+						"entries": [
+							"The leshy transforms into a {@trait Small} plant.",
+							"This ability otherwise uses the effects of {@spell tree shape}."
+						],
+						"generic": {
+							"tag": "ability"
+						}
+					},
+					{
+						"name": "Bend Back",
+						"activity": {
+							"number": 1,
+							"unit": "action"
+						},
+						"entries": [
+							"The golden bamboo leshy uses an action to bend itself back, ready to whip forward, increasing its reach with its fist to 10 feet. After the golden bamboo leshy {@action Strikes} with its fist, it's no longer Bent Back."
+						]
+					},
+					{
+						"name": "Coiled Opportunity",
+						"activity": {
+							"number": 1,
+							"unit": "reaction"
+						},
+						"entries": [
+							"As {@ability Attack of Opportunity}, but the golden bamboo leshy can use this reaction only if it's Bent Back."
+						]
+					}
+				]
+			}
+		},
+		{
+			"name": "Minkaian Honeysuckle Leshy",
+			"source": "AFoF",
+			"page": 11,
+			"level": 2,
+			"traits": [
+				"unique",
+				"n",
+				"small",
+				"leshy",
+				"plant"
+			],
+			"perception": {
+				"std": 8
+			},
+			"senses": [
+				{
+					"name": "low-light vision"
+				}
+			],
+			"languages": {
+				"languages": [
+					"common",
+					"druidic",
+					"sylvan"
+				],
+				"abilities": [
+					"{@spell speak with plants} (same type as leshy)"
+				]
+			},
+			"skills": {
+				"athletics": {
+					"std": 6
+				},
+				"nature": {
+					"std": 6
+				},
+				"stealth": {
+					"std": 8
+				}
+			},
+			"abilityMods": {
+				"str": 2,
+				"dex": 4,
+				"con": 2,
+				"int": -1,
+				"wis": 2,
+				"cha": 0
+			},
+			"defenses": {
+				"ac": {
+					"std": 18
+				},
+				"savingThrows": {
+					"fort": {
+						"std": 8
+					},
+					"ref": {
+						"std": 10
+					},
+					"will": {
+						"std": 6
+					},
+					"abilities": [
+						"+1 status bonus to Reflex and Fortitude saves and DCs against {@action trip} and {@quickref forced movement|CRB|3|forced movement|0}"
+					]
+				},
+				"hp": [
+					{
+						"hp": 30
+					}
+				],
+				"resistances": [
+					{
+						"amount": 3,
+						"name": "physical"
+					}
+				]
+			},
+			"speed": {
+				"walk": 25
+			},
+			"attacks": [
+				{
+					"range": "Melee",
+					"traits": [
+						"agile",
+						"finesse"
+					],
+					"name": "fist",
+					"attack": 10,
+					"damage": "{@damage 1d6+4} bludgeoning"
+				}
+			],
+			"spellcasting": [
+				{
+					"tradition": "primal",
+					"type": "Innate",
+					"DC": 16,
+					"entry": {
+						"4": {
+							"spells": [
+								{
+									"name": "speak with plants"
+								}
+							]
+						}
+					}
+				}
+			],
+			"abilities": {
+				"mid": [
+					{
+						"name": "Verdant Burst",
+						"traits": [
+							"healing"
+						],
+						"entries": [
+							"When a leshy dies, a burst of primal energy explodes from its body, restoring {@dice 2d8} Hit Points to each plant creature in a 30-foot emanation. This area is filled with plants of the same type as the leshy, becoming difficult terrain. If the terrain isn't a viable environment for these plants, they wither after 24 hours."
+						]
+					}
+				],
+				"bot": [
+					{
+						"name": "Change Shape",
+						"activity": {
+							"number": 1,
+							"unit": "action"
+						},
+						"traits": [
+							"concentrate",
+							"polymorph",
+							"primal",
+							"transmutation"
+						],
+						"entries": [
+							"The leshy transforms into a {@trait Small} plant.",
+							"This ability otherwise uses the effects of {@spell tree shape}."
+						],
+						"generic": {
+							"tag": "ability"
+						}
+					}
+				]
+			}
+		},
+		{
+			"name": "Mint Leshy",
+			"source": "AFoF",
+			"page": 11,
+			"level": 2,
+			"traits": [
+				"unique",
+				"n",
+				"medium",
+				"leshy",
+				"plant"
+			],
+			"perception": {
+				"std": 8
+			},
+			"senses": [
+				{
+					"name": "low-light vision"
+				}
+			],
+			"languages": {
+				"languages": [
+					"common",
+					"druidic",
+					"sylvan"
+				],
+				"abilities": [
+					"{@spell speak with plants} (same type as leshy)"
+				]
+			},
+			"skills": {
+				"athletics": {
+					"std": 6
+				},
+				"nature": {
+					"std": 6
+				},
+				"stealth": {
+					"std": 8
+				}
+			},
+			"abilityMods": {
+				"str": 2,
+				"dex": 4,
+				"con": 2,
+				"int": -1,
+				"wis": 2,
+				"cha": 0
+			},
+			"defenses": {
+				"ac": {
+					"std": 18
+				},
+				"savingThrows": {
+					"fort": {
+						"std": 8
+					},
+					"ref": {
+						"std": 10
+					},
+					"will": {
+						"std": 6
+					}
+				},
+				"hp": [
+					{
+						"hp": 30
+					}
+				]
+			},
+			"speed": {
+				"walk": 25
+			},
+			"attacks": [
+				{
+					"range": "Melee",
+					"traits": [
+						"agile",
+						"finesse"
+					],
+					"name": "fist",
+					"attack": 10,
+					"damage": "{@damage 1d8+4} bludgeoning"
+				}
+			],
+			"spellcasting": [
+				{
+					"tradition": "primal",
+					"type": "Innate",
+					"DC": 16,
+					"entry": {
+						"4": {
+							"spells": [
+								{
+									"name": "speak with plants"
+								}
+							]
+						}
+					}
+				}
+			],
+			"abilities": {
+				"mid": [
+					{
+						"name": "Verdant Burst",
+						"traits": [
+							"healing"
+						],
+						"entries": [
+							"When a leshy dies, a burst of primal energy explodes from its body, restoring {@dice 2d8} Hit Points to each plant creature in a 30-foot emanation. This area is filled with plants of the same type as the leshy, becoming difficult terrain. If the terrain isn't a viable environment for these plants, they wither after 24 hours."
+						]
+					}
+				],
+				"bot": [
+					{
+						"name": "Change Shape",
+						"activity": {
+							"number": 1,
+							"unit": "action"
+						},
+						"traits": [
+							"concentrate",
+							"polymorph",
+							"primal",
+							"transmutation"
+						],
+						"entries": [
+							"The leshy transforms into a {@trait Small} plant.",
+							"This ability otherwise uses the effects of {@spell tree shape}."
+						],
+						"generic": {
+							"tag": "ability"
+						}
+					},
+					{
+						"name": "Engulf",
+						"activity": {
+							"number": 2,
+							"unit": "action"
+						},
+						"entries": [
+							"{@dc 18}, {@damage 1d6} bludgeoning plus {@damage 1d6} poison, {@action Escape} {@dc 18}, Rupture 6"
+						],
+						"generic": {
+							"tag": "ability"
+						}
+					}
+				]
+			}
+		},
+		{
+			"name": "Wisteria Leshy",
+			"source": "AFoF",
+			"page": 11,
+			"level": 2,
+			"traits": [
+				"unique",
+				"n",
+				"small",
+				"leshy",
+				"plant"
+			],
+			"perception": {
+				"std": 8
+			},
+			"senses": [
+				{
+					"name": "low-light vision"
+				}
+			],
+			"languages": {
+				"languages": [
+					"common",
+					"druidic",
+					"sylvan"
+				],
+				"abilities": [
+					"{@spell speak with plants} (same type as leshy)"
+				]
+			},
+			"skills": {
+				"athletics": {
+					"std": 6
+				},
+				"nature": {
+					"std": 6
+				},
+				"stealth": {
+					"std": 8
+				}
+			},
+			"abilityMods": {
+				"str": 2,
+				"dex": 4,
+				"con": 2,
+				"int": -1,
+				"wis": 2,
+				"cha": 0
+			},
+			"defenses": {
+				"ac": {
+					"std": 18
+				},
+				"savingThrows": {
+					"fort": {
+						"std": 8
+					},
+					"ref": {
+						"std": 10
+					},
+					"will": {
+						"std": 6
+					}
+				},
+				"hp": [
+					{
+						"hp": 30
+					}
+				]
+			},
+			"speed": {
+				"walk": 25
+			},
+			"attacks": [
+				{
+					"range": "Melee",
+					"traits": [
+						"agile",
+						"finesse"
+					],
+					"name": "fist",
+					"attack": 10,
+					"damage": "{@damage 1d6+4} bludgeoning plus Grab"
+				}
+			],
+			"spellcasting": [
+				{
+					"tradition": "primal",
+					"type": "Innate",
+					"DC": 16,
+					"entry": {
+						"4": {
+							"spells": [
+								{
+									"name": "speak with plants"
+								}
+							]
+						}
+					}
+				}
+			],
+			"abilities": {
+				"mid": [
+					{
+						"name": "Verdant Burst",
+						"traits": [
+							"healing"
+						],
+						"entries": [
+							"When a leshy dies, a burst of primal energy explodes from its body, restoring {@dice 2d8} Hit Points to each plant creature in a 30-foot emanation. This area is filled with plants of the same type as the leshy, becoming difficult terrain. If the terrain isn't a viable environment for these plants, they wither after 24 hours."
+						]
+					}
+				],
+				"bot": [
+					{
+						"name": "Change Shape",
+						"activity": {
+							"number": 1,
+							"unit": "action"
+						},
+						"traits": [
+							"concentrate",
+							"polymorph",
+							"primal",
+							"transmutation"
+						],
+						"entries": [
+							"The leshy transforms into a {@trait Small} plant.",
+							"This ability otherwise uses the effects of {@spell tree shape}."
+						],
+						"generic": {
+							"tag": "ability"
+						}
+					},
+					{
+						"name": "Constrict",
+						"activity": {
+							"number": 1,
+							"unit": "action"
+						},
+						"entries": [
+							"{@damage 1d6+4} bludgeoning, {@dc 18}"
+						],
+						"generic": {
+							"tag": "ability"
+						}
+					}
+				]
+			}
+		}
+	]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 30d1469..11d2af8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,6 +10,7 @@
         "moduleResolution": "node",
         "importHelpers": true,
         "experimentalDecorators": true,
+        "esModuleInterop": true,
         "types": ["svelte", "node"],
         "lib": ["dom", "esnext", "scripthost", "es2020"],
         "isolatedModules": false


Reviews:
user: login: clausman
body: Changes look good overall. The file settings.ts also needs updated so that the new importer shows up in the UI. You can mimic the pattern used by importPF2EMonsterTools
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-05-20T02:17:36Z
user: login: clausman
body: There seems to be a bug in pf2eToolsImport.ts when I try to use it to import this file https://github.com/Pf2eToolsOrg/Pf2eTools/blob/dev/data/bestiary/creatures-afof.json. Could you please setup jest unit tests for the project and add a unit test which tests the function `convertPf2eToolsCreatureToMonster` on some test data which is copy-pasted from https://github.com/Pf2eToolsOrg/Pf2eTools/blob/dev/data/bestiary/creatures-afof.json ? 
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-05-20T02:43:41Z
user: login: clausman
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-05-20T13:55:54Z
user: login: copilot-swe-agent[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-20T14:02:18Z

Review comments:
diff_hunk: @@ -0,0 +1,430 @@
+import { Monster, Trait } from "../../index";
+import { ONE_ACTION, addSign, getACStats, getModifierToDiceRoll, toTitleCase } from "./pf2eMonsterToolImport";
+
+export interface Pf2eToolsCreature {
+    name: string;
+    source: string;
+    page?: number;
+    level: number;
+    traits: string[];
+    perception: {
+        std: number;
+    };
+    senses?: {
+        name: string;
+    }[];
+    languages?: {
+        languages?: string[];
+        abilities?: string[];
+    };
+    skills?: Record<string, {
+        std: number;
+        note?: string;
+    }>;
+    abilityMods: {
+        str: number;
+        dex: number;
+        con: number;
+        int: number;
+        wis: number;
+        cha: number;
+    };
+    items?: string[];
+    speed: {
+        walk?: number;
+        fly?: number;
+        swim?: number;
+        climb?: number;
+        burrow?: number;
+    };
+    attacks?: {
+        range: "Melee" | "Ranged";
+        name: string;
+        attack: number;
+        traits?: string[];
+        damage?: string;
+        types?: string[];
+    }[];
+    spellcasting?: {
+        type: string;
+        tradition?: string;
+        DC?: number;
+        fp?: number;
+        name?: string;
+        entry: Record<string, {
+            level?: number;
+            spells: {
+                name: string;
+            }[];
+        }>;
+    }[];
+    abilities?: {
+        top?: Ability[];
+        mid?: Ability[];
+        bot?: Ability[];
+    };
+    defenses: {
+        ac: {
+            std: number;
+            [key: string]: number;
+        };
+        savingThrows: {
+            fort: {
+                std: number;
+            };
+            ref: {
+                std: number;
+            };
+            will: {
+                std: number;
+            };
+        };
+        hp: {
+            hp: number;
+            note?: string;
+        }[];
+        immunities?: string[];
+        resistances?: {
+            amount: number;
+            name: string;
+            note?: string;
+        }[];
+        weaknesses?: {
+            amount: number;
+            name: string;
+            note?: string;
+        }[];
+    };
+}
+
+interface Ability {
+    name: string;
+    entries?: (string | { type: string; items: string[] })[];
+    activity?: {
+        number: number;
+        unit: string;
+    };
+    trigger?: string;
+}
+
+export async function buildMonsterFromPf2eToolsFile(
+    file: File
+): Promise<Monster[]> {
+    return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.onload = async (event: any) => {
+            try {
+                let json = JSON.parse(event.target.result);
+                const monsters: Monster[] = [];
+
+                // The Pf2eTools format has creatures in a "creature" array
+                if (json.creature && Array.isArray(json.creature)) {
path: src/importers/pf2eToolsImport.ts
user: login: clausman
body: Make the conversation and pushing to the monsters array tolerant to errors in the convert function by catching any exceptions, logging them, and then skipping that monster. Make sure to update the unit tests to check this error handling works as expected.
created_at: 2025-05-20T13:54:46Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,430 @@
+import { Monster, Trait } from "../../index";
+import { ONE_ACTION, addSign, getACStats, getModifierToDiceRoll, toTitleCase } from "./pf2eMonsterToolImport";
+
+export interface Pf2eToolsCreature {
+    name: string;
+    source: string;
+    page?: number;
+    level: number;
+    traits: string[];
+    perception: {
+        std: number;
+    };
+    senses?: {
+        name: string;
+    }[];
+    languages?: {
+        languages?: string[];
+        abilities?: string[];
+    };
+    skills?: Record<string, {
+        std: number;
+        note?: string;
+    }>;
+    abilityMods: {
+        str: number;
+        dex: number;
+        con: number;
+        int: number;
+        wis: number;
+        cha: number;
+    };
+    items?: string[];
+    speed: {
+        walk?: number;
+        fly?: number;
+        swim?: number;
+        climb?: number;
+        burrow?: number;
+    };
+    attacks?: {
+        range: "Melee" | "Ranged";
+        name: string;
+        attack: number;
+        traits?: string[];
+        damage?: string;
+        types?: string[];
+    }[];
+    spellcasting?: {
+        type: string;
+        tradition?: string;
+        DC?: number;
+        fp?: number;
+        name?: string;
+        entry: Record<string, {
+            level?: number;
+            spells: {
+                name: string;
+            }[];
+        }>;
+    }[];
+    abilities?: {
+        top?: Ability[];
+        mid?: Ability[];
+        bot?: Ability[];
+    };
+    defenses: {
+        ac: {
+            std: number;
+            [key: string]: number;
+        };
+        savingThrows: {
+            fort: {
+                std: number;
+            };
+            ref: {
+                std: number;
+            };
+            will: {
+                std: number;
+            };
+        };
+        hp: {
+            hp: number;
+            note?: string;
+        }[];
+        immunities?: string[];
+        resistances?: {
+            amount: number;
+            name: string;
+            note?: string;
+        }[];
+        weaknesses?: {
+            amount: number;
+            name: string;
+            note?: string;
+        }[];
+    };
+}
+
+interface Ability {
+    name: string;
+    entries?: (string | { type: string; items: string[] })[];
+    activity?: {
+        number: number;
+        unit: string;
+    };
+    trigger?: string;
+}
+
+export async function buildMonsterFromPf2eToolsFile(
+    file: File
+): Promise<Monster[]> {
+    return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.onload = async (event: any) => {
+            try {
+                let json = JSON.parse(event.target.result);
+                const monsters: Monster[] = [];
+
+                // The Pf2eTools format has creatures in a "creature" array
+                if (json.creature && Array.isArray(json.creature)) {
path: src/importers/pf2eToolsImport.ts
user: login: Copilot
body: I've updated the code to make the conversion process error-tolerant. The importer now catches exceptions during individual monster conversions, logs the errors, and continues with the next monster. I've also added comprehensive unit tests to verify this behavior works as expected. You can see the changes in commit 5173820.
created_at: 2025-05-20T14:02:17Z
author_association: NONE

PR comments:
user: login: Copilot
created_at: 2025-05-20T02:23:03Z
body: 
> Changes look good overall. The file settings.ts also needs updated so that the new importer shows up in the UI. You can mimic the pattern used by importPF2EMonsterTools

I've updated the settings.ts file to add the Pf2eTools importer to the UI. I followed the same pattern as the PF2EMonsterTools importer by adding a new Setting with a file input and appropriate event handlers. The changes have been tested and build successfully. Commit: bd1c1ee
author_association: NONE
user: login: Copilot
created_at: 2025-05-20T02:50:33Z
body: 
> There seems to be a bug in pf2eToolsImport.ts when I try to use it to import this file https://github.com/Pf2eToolsOrg/Pf2eTools/blob/dev/data/bestiary/creatures-afof.json. Could you please setup jest...

I've set up Jest unit tests for the project and added tests for the `convertPf2eToolsCreatureToMonster` function as requested. The tests use actual data from the Pf2eTools repository, including creatures from the creatures-afof.json file. Each test verifies different aspects of the conversion process and handles various edge cases. All tests are now passing successfully. Commit: 9785412
author_association: NONE
user: login: clausman
created_at: 2025-05-20T05:21:13Z
body: It looks like you forgot to add a new github action for that runs on pull requests that runs the jest unit tests. Please set that up and ensure it runs and succeeds on this PR
author_association: OWNER
user: login: Copilot
created_at: 2025-05-20T05:24:41Z
body: 
> It looks like you forgot to add a new github action for that runs on pull requests that runs the jest unit tests. Please set that up and ensure it runs and succeeds on this PR

I've added a GitHub Action workflow that runs Jest unit tests on pull requests and pushes to the main branch. The workflow file is set up to install dependencies and run the test command. I've verified that the tests run successfully locally. You can see the changes in commit 75215e6.
author_association: NONE