pf2e-party-tracker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "PF2e Party Tracker",
3
+ "short_name": "Party Tracker",
4
+ "description": "A GM's Pathfinder 2e party tracker — passive stats, secret checks, exploration activities, offline.",
5
+ "start_url": ".",
6
+ "scope": ".",
7
+ "display": "standalone",
8
+ "orientation": "landscape",
9
+ "background_color": "#14161b",
10
+ "theme_color": "#14161b",
11
+ "icons": [
12
+ { "src": "icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }
13
+ ]
14
+ }
package/dist/sw.js ADDED
@@ -0,0 +1,62 @@
1
+ /* Service worker — offline support for the hosted (GitHub Pages) PWA.
2
+ Network-first for the app page (fresh when online, cached when offline),
3
+ cache-first for static assets. Cache version is stamped at build time so a
4
+ redeploy refreshes clients automatically. */
5
+ const CACHE = "pf2e-party-tracker-20260914005107";
6
+ /* Every cache this app has ever made starts with PREFIX. The sweep below is
7
+ scoped to it on purpose: the three PF2e tools share an origin when they are
8
+ served from one host (GitHub Pages does it, and so does the toolbox), and an
9
+ unscoped sweep would delete the other two apps' offline caches every time
10
+ this one activated. */
11
+ const PREFIX = "pf2e-party-tracker-";
12
+ const SHELL = ["./", "./index.html", "./manifest.webmanifest", "./icon.svg"];
13
+
14
+ self.addEventListener("install", (e) => {
15
+ e.waitUntil(
16
+ caches.open(CACHE)
17
+ .then((c) => c.addAll(SHELL).catch(() => {}))
18
+ .then(() => self.skipWaiting())
19
+ );
20
+ });
21
+
22
+ self.addEventListener("activate", (e) => {
23
+ e.waitUntil(
24
+ caches.keys()
25
+ .then((keys) => Promise.all(keys.filter((k) => k.startsWith(PREFIX) && k !== CACHE)
26
+ .map((k) => caches.delete(k))))
27
+ .then(() => self.clients.claim())
28
+ );
29
+ });
30
+
31
+ self.addEventListener("fetch", (e) => {
32
+ const req = e.request;
33
+ if (req.method !== "GET") return;
34
+
35
+ // App page: network-first so deploys are picked up, cache fallback when offline.
36
+ if (req.mode === "navigate") {
37
+ e.respondWith(
38
+ fetch(req)
39
+ .then((res) => {
40
+ const copy = res.clone();
41
+ caches.open(CACHE).then((c) => c.put("./index.html", copy));
42
+ return res;
43
+ })
44
+ .catch(() => caches.match("./index.html").then((r) => r || caches.match("./")))
45
+ );
46
+ return;
47
+ }
48
+
49
+ // Static assets: cache-first.
50
+ e.respondWith(
51
+ caches.match(req).then((r) =>
52
+ r ||
53
+ fetch(req).then((res) => {
54
+ if (res.ok && new URL(req.url).origin === self.location.origin) {
55
+ const copy = res.clone();
56
+ caches.open(CACHE).then((c) => c.put(req, copy));
57
+ }
58
+ return res;
59
+ }).catch(() => r)
60
+ )
61
+ );
62
+ });
package/notice.md ADDED
@@ -0,0 +1,81 @@
1
+ # Third-Party Notices & Attribution
2
+
3
+ **pf2e-party-tracker**
4
+
5
+ This file records the licensing of the two distinct kinds of material in this
6
+ package: the application **code**, and the Pathfinder **rules text** it bundles
7
+ (condition and action descriptions). They are covered by different licenses and
8
+ the distinction below is intentional.
9
+
10
+ ---
11
+
12
+ ## 1. Application code
13
+
14
+ All original source code in this package is released under the **MIT License**.
15
+ See the [`LICENSE`](./LICENSE) file for the full text.
16
+
17
+ ```
18
+ Copyright (c) 2026 Whyte Erminae
19
+ ```
20
+
21
+ The MIT License covers only the code authored for this project. It does **not**
22
+ cover the Pathfinder game content described in section 2, which carries its own
23
+ license and notice. The character **numbers** shown by this app are computed from
24
+ data you import from your own Pathbuilder builds; no character data is bundled.
25
+
26
+ ---
27
+
28
+ ## 2. Pathfinder rules content (ORC License)
29
+
30
+ The bundled reference data consists of the **rules expressions of Pathfinder
31
+ Second Edition conditions and actions** — their names and functional effect text.
32
+ Under the Open RPG Creative License this is **Licensed Material** (the copyrighted
33
+ expression of game mechanics), used here under that license. It originates with
34
+ **Paizo Inc.** and is derived from the open-source [Foundry VTT `pf2e`
35
+ system](https://github.com/foundryvtt/pf2e) data.
36
+
37
+ ### ORC Notice
38
+
39
+ This product is licensed under the ORC License located at the Library of Congress
40
+ at TX 9-307-067 and available online at various locations including
41
+ paizo.com/orclicense, azoralaw.com/orclicense, and others. All warranties are
42
+ disclaimed as set forth therein.
43
+
44
+ ### Attribution
45
+
46
+ This product is based on the following Licensed Material:
47
+
48
+ Pathfinder Player Core © 2023 Paizo Inc., Designed by Logan Bonner, Jason
49
+ Bulmahn, Stephen Radney-MacFarland, and Mark Seifter.
50
+
51
+ Pathfinder Player Core 2 © 2024 Paizo Inc.
52
+
53
+ Pathfinder GM Core © 2023 Paizo Inc., Designed by Logan Bonner and Mark Seifter.
54
+
55
+ > NOTE — The precise list of source books is generated into `GENERATED_REF_META`
56
+ > in `data/reference.generated.js` from the licenses recorded in the Foundry pf2e
57
+ > data. Re-sync this section whenever the reference data is rebuilt
58
+ > (`npm run build:ref`).
59
+
60
+ ### Reserved Material
61
+
62
+ Reserved Material elements are excluded from this product. To avoid confusion,
63
+ such items include all trademarks, registered trademarks, and proper nouns,
64
+ artwork, dialogue, organizations, plots, and storylines. Where a rule's mechanics
65
+ are included, any such Reserved Material has been omitted or genericized.
66
+
67
+ ### Crediting this work downstream
68
+
69
+ pf2e-party-tracker © 2026 Whyte Erminae.
70
+
71
+ ---
72
+
73
+ ## 3. Compatibility / trademark notice
74
+
75
+ Pathfinder and all associated marks and logos are trademarks of Paizo Inc.
76
+ This package is an independent work, is **not** published, endorsed, sponsored,
77
+ or affiliated with Paizo Inc., and no such association is implied. Any reference
78
+ to Pathfinder is made under nominative fair use solely to identify the game
79
+ system with which this material is compatible. "Pathbuilder" is a third-party
80
+ tool by Redrazors; this project is not affiliated with it and only reads the JSON
81
+ you choose to export.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "pf2e-party-tracker",
3
+ "version": "0.1.0",
4
+ "description": "A GM's Pathfinder 2e party tracker — passive stats, secret checks, exploration activities (single self-contained HTML).",
5
+ "keywords": [
6
+ "pathfinder",
7
+ "pf2e",
8
+ "gm",
9
+ "party",
10
+ "tracker",
11
+ "ttrpg"
12
+ ],
13
+ "homepage": "https://thewhytewolf.github.io/pf2epartytracker/",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/TheWhyteWolf/pf2epartytracker.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/TheWhyteWolf/pf2epartytracker/issues"
20
+ },
21
+ "author": "Whyte",
22
+ "license": "SEE LICENSE IN LICENSE",
23
+ "type": "module",
24
+ "bin": {
25
+ "pf2e-party-tracker": "bin/cli.mjs"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "files": [
31
+ "bin/",
32
+ "src/",
33
+ "tools/",
34
+ "data/",
35
+ "dist/",
36
+ "notice.md"
37
+ ],
38
+ "scripts": {
39
+ "build": "python3 tools/build.py",
40
+ "build:ref": "python3 tools/build_reference.py --src \"$PF2E_DATA/packs/pf2e\" --out data/reference.generated.js",
41
+ "test": "node tools/test.mjs",
42
+ "check": "npm run build && npm test"
43
+ },
44
+ "devDependencies": {
45
+ "jsdom": "^24.0.0"
46
+ }
47
+ }
package/src/config.js ADDED
@@ -0,0 +1,140 @@
1
+ /* ============================================================
2
+ STATIC CONFIG — PF2e (Remaster) structure the engine consumes.
3
+ Game-math mappings (skill→ability, save→ability, exploration
4
+ activity→governing skill) are hand-authored here; prose comes from
5
+ the Foundry-derived data/reference.generated.js at build time.
6
+ ============================================================ */
7
+
8
+ /* The six ability scores, in canonical order. */
9
+ const ABILITIES = ["str", "dex", "con", "int", "wis", "cha"];
10
+ const ABILITY_LABEL = { str: "STR", dex: "DEX", con: "CON", int: "INT", wis: "WIS", cha: "CHA" };
11
+
12
+ /* The 16 trained skills and their key ability (Pathbuilder uses these exact keys
13
+ in build.proficiencies). */
14
+ const SKILLS = [
15
+ { key: "acrobatics", label: "Acrobatics", ability: "dex" },
16
+ { key: "arcana", label: "Arcana", ability: "int" },
17
+ { key: "athletics", label: "Athletics", ability: "str" },
18
+ { key: "crafting", label: "Crafting", ability: "int" },
19
+ { key: "deception", label: "Deception", ability: "cha" },
20
+ { key: "diplomacy", label: "Diplomacy", ability: "cha" },
21
+ { key: "intimidation", label: "Intimidation", ability: "cha" },
22
+ { key: "medicine", label: "Medicine", ability: "wis" },
23
+ { key: "nature", label: "Nature", ability: "wis" },
24
+ { key: "occultism", label: "Occultism", ability: "int" },
25
+ { key: "performance", label: "Performance", ability: "cha" },
26
+ { key: "religion", label: "Religion", ability: "wis" },
27
+ { key: "society", label: "Society", ability: "int" },
28
+ { key: "stealth", label: "Stealth", ability: "dex" },
29
+ { key: "survival", label: "Survival", ability: "wis" },
30
+ { key: "thievery", label: "Thievery", ability: "dex" },
31
+ ];
32
+ const SKILL_ABILITY = {};
33
+ SKILLS.forEach((s) => { SKILL_ABILITY[s.key] = s.ability; });
34
+
35
+ /* The three saving throws. */
36
+ const SAVES = [
37
+ { key: "fortitude", label: "Fort", full: "Fortitude", ability: "con" },
38
+ { key: "reflex", label: "Ref", full: "Reflex", ability: "dex" },
39
+ { key: "will", label: "Will", full: "Will", ability: "wis" },
40
+ ];
41
+
42
+ const PERCEPTION = { ability: "wis" };
43
+
44
+ /* Proficiency rank-value (0/2/4/6/8) → readable label. */
45
+ const RANK_LABEL = { 0: "Untrained", 2: "Trained", 4: "Expert", 6: "Master", 8: "Legendary" };
46
+ const RANK_ABBR = { 0: "U", 2: "T", 4: "E", 6: "M", 8: "L" };
47
+
48
+ /* Special senses to detect inside a Pathbuilder build's `specials` array
49
+ (matched case-insensitively). */
50
+ const SENSES = [
51
+ "Low-Light Vision", "Darkvision", "Greater Darkvision", "Scent", "Tremorsense",
52
+ "Lifesense", "Echolocation", "Wavesense", "Truesight", "See the Unseen",
53
+ ];
54
+
55
+ /* The exploration activities (GM Core). `governing` is the skill/statistic a GM
56
+ most often references for the activity — a skill key, "perception", "varies"
57
+ (depends on what's being done), or "none" (no roll). `actionSlug` links to the
58
+ full rules text in the generated reference data. */
59
+ const EXPLORATION_ACTIVITIES = [
60
+ { key: "avoid-notice", label: "Avoid Notice", governing: "stealth", actionSlug: "avoid-notice",
61
+ short: "Use Stealth to avoid being noticed while traveling." },
62
+ { key: "cover-tracks", label: "Cover Tracks", governing: "survival", actionSlug: "cover-tracks",
63
+ short: "Use Survival to obscure the party's trail." },
64
+ { key: "defend", label: "Defend", governing: "none", actionSlug: "defend",
65
+ short: "Travel at half Speed with your shield raised, ready for danger." },
66
+ { key: "detect-magic", label: "Detect Magic", governing: "none", actionSlug: "detect-magic",
67
+ short: "Cast detect magic at intervals as you travel." },
68
+ { key: "follow-the-expert", label: "Follow the Expert", governing: "varies", actionSlug: "follow-the-expert",
69
+ short: "Match an ally's activity, adding their proficiency to your check." },
70
+ { key: "hustle", label: "Hustle", governing: "none", actionSlug: "hustle",
71
+ short: "Move at double travel Speed for a time based on your Constitution." },
72
+ { key: "investigate", label: "Investigate", governing: "varies", actionSlug: "investigate",
73
+ short: "Recall Knowledge as you travel (the relevant knowledge skill)." },
74
+ { key: "repeat-a-spell", label: "Repeat a Spell", governing: "none", actionSlug: "repeat-a-spell",
75
+ short: "Sustain a spell repeatedly as you travel." },
76
+ { key: "scout", label: "Scout", governing: "none", actionSlug: "scout",
77
+ short: "Scout ahead, granting the party a bonus to initiative." },
78
+ { key: "search", label: "Search", governing: "perception", actionSlug: "search",
79
+ short: "Use Perception to look for hidden creatures, doors, and hazards." },
80
+ { key: "track", label: "Track", governing: "survival", actionSlug: "track",
81
+ short: "Use Survival to follow tracks as you travel." },
82
+ ];
83
+ const EXPLORATION_BY_KEY = {};
84
+ EXPLORATION_ACTIVITIES.forEach((a) => { EXPLORATION_BY_KEY[a.key] = a; });
85
+
86
+ /* Conditions surfaced as quick-add buttons in live tracking. `valued` conditions
87
+ carry a numeric value (Frightened 2). Wounded/Dying/Doomed have dedicated
88
+ steppers and are intentionally not listed here. */
89
+ const QUICK_CONDITIONS = [
90
+ { key: "frightened", label: "Frightened", valued: true },
91
+ { key: "sickened", label: "Sickened", valued: true },
92
+ { key: "clumsy", label: "Clumsy", valued: true },
93
+ { key: "enfeebled", label: "Enfeebled", valued: true },
94
+ { key: "drained", label: "Drained", valued: true },
95
+ { key: "stupefied", label: "Stupefied", valued: true },
96
+ { key: "slowed", label: "Slowed", valued: true },
97
+ { key: "stunned", label: "Stunned", valued: true },
98
+ { key: "off-guard", label: "Off-Guard", valued: false },
99
+ { key: "prone", label: "Prone", valued: false },
100
+ { key: "fatigued", label: "Fatigued", valued: false },
101
+ { key: "blinded", label: "Blinded", valued: false },
102
+ { key: "dazzled", label: "Dazzled", valued: false },
103
+ { key: "deafened", label: "Deafened", valued: false },
104
+ { key: "grabbed", label: "Grabbed", valued: false },
105
+ { key: "restrained", label: "Restrained", valued: false },
106
+ { key: "unconscious", label: "Unconscious", valued: false },
107
+ ];
108
+
109
+ /* ============================================================
110
+ INLINE SVG ICON REGISTRY
111
+ Thin line icons (24x24, stroke = currentColor) so they inherit the
112
+ active accent. Keyed by name; iconSvg() wraps the path in an <svg>.
113
+ ============================================================ */
114
+ const ICONS = {
115
+ /* navigation */
116
+ roster: '<path d="M3 5h18M3 12h18M3 19h18"/><circle cx="6.5" cy="5" r="1.3"/><circle cx="6.5" cy="12" r="1.3"/><circle cx="6.5" cy="19" r="1.3"/>',
117
+ roll: '<rect x="4" y="4" width="16" height="16" rx="3"/><circle cx="8.5" cy="8.5" r="1.1" fill="currentColor" stroke="none"/><circle cx="15.5" cy="8.5" r="1.1" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.1" fill="currentColor" stroke="none"/><circle cx="8.5" cy="15.5" r="1.1" fill="currentColor" stroke="none"/><circle cx="15.5" cy="15.5" r="1.1" fill="currentColor" stroke="none"/>',
118
+ explore: '<circle cx="12" cy="12" r="9"/><path d="M15.5 8.5 13 13l-4.5 2.5L11 11z"/>',
119
+ reference: '<path d="M5 4.5A1.5 1.5 0 0 1 6.5 3H19v15H6.5A1.5 1.5 0 0 0 5 19.5z"/><path d="M5 19.5A1.5 1.5 0 0 1 6.5 18H19v3H6.5A1.5 1.5 0 0 1 5 19.5z"/>',
120
+ menu: '<circle cx="12" cy="8" r="3.4"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/>',
121
+ /* stats */
122
+ shield: '<path d="M12 3 5 5.5v5.5c0 4 2.9 7 7 8.5 4.1-1.5 7-4.5 7-8.5V5.5z"/>',
123
+ eye: '<path d="M2.5 12S6 6 12 6s9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6z"/><circle cx="12" cy="12" r="2.4"/>',
124
+ heart: '<path d="M12 20s-7-4.4-7-9.5A3.8 3.8 0 0 1 12 8a3.8 3.8 0 0 1 7 2.5C19 15.6 12 20 12 20z"/>',
125
+ star: '<path d="M12 3.5l2.5 5.3 5.5.7-4 4 1 5.6L12 16.5 6.9 19l1-5.6-4-4 5.5-.7z"/>',
126
+ boot: '<path d="M7 3v10l-2 2v4h13v-2c0-2-2-2.5-2-4V9"/><path d="M7 13h5"/>',
127
+ /* utility */
128
+ plus: '<path d="M12 5v14M5 12h14"/>',
129
+ minus: '<path d="M5 12h14"/>',
130
+ chevron: '<path d="M6 9l6 6 6-6"/>',
131
+ x: '<path d="M6 6l12 12M18 6 6 18"/>',
132
+ search: '<circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/>',
133
+ install: '<path d="M12 3v11M8 10l4 4 4-4"/><path d="M5 20h14"/>',
134
+ up: '<path d="M12 19V5M6 11l6-6 6 6"/>',
135
+ down: '<path d="M12 5v14M6 13l6 6 6-6"/>',
136
+ };
137
+ function iconSvg(name, cls) {
138
+ const p = ICONS[name] || "";
139
+ return `<svg class="icn${cls ? " " + cls : ""}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${p}</svg>`;
140
+ }